feat(org): add the kvb-org-rasci RASCI/OKR register and kvb-org-artefacts
rasci.py keeps the organisation's tasks in <council root>/rasci.json: RASCI roles, objective with key results, priority, worker, start, end, workdir, branch, tools and artifacts; done is refused while a key result is open. council.py writes every claim, takeover and release through, and show lists the live claims as one row per task with R, A, S, C and I columns. kvb-org-artefacts manages the artifacts in the same table for kvb-cleanup. The org, plan, triage, tune, cleanup, branch-gc and dream skills now record into and read from the register; five descriptions were tightened to keep the listing budget. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
624baccfc4
commit
35dc37641d
23 files changed
+866
-44
No files matched your search
@@ -57,6 +57,8 @@ flowchart LR
|
|||||||
org --> org_coordinate[kvb-org-coordinate]
|
org --> org_coordinate[kvb-org-coordinate]
|
||||||
org --> org_perform[kvb-org-perform]
|
org --> org_perform[kvb-org-perform]
|
||||||
org --> org_matrix[kvb-org-matrix]
|
org --> org_matrix[kvb-org-matrix]
|
||||||
|
org --> org_rasci[kvb-org-rasci]
|
||||||
|
org --> org_artefacts[kvb-org-artefacts]
|
||||||
org --> org_robot[kvb-org-robot]
|
org --> org_robot[kvb-org-robot]
|
||||||
skills --> skills_dream[kvb-skills-dream]
|
skills --> skills_dream[kvb-skills-dream]
|
||||||
skills --> skills_tune[kvb-skills-tune]
|
skills --> skills_tune[kvb-skills-tune]
|
||||||
@@ -124,6 +126,8 @@ flowchart LR
|
|||||||
coordinate[kvb-org-coordinate]
|
coordinate[kvb-org-coordinate]
|
||||||
subagent_model[kvb-org-perform]
|
subagent_model[kvb-org-perform]
|
||||||
status_matrix[kvb-org-matrix]
|
status_matrix[kvb-org-matrix]
|
||||||
|
rasci[kvb-org-rasci]
|
||||||
|
artefacts[kvb-org-artefacts]
|
||||||
robot[kvb-org-robot]
|
robot[kvb-org-robot]
|
||||||
end
|
end
|
||||||
subgraph cat_skills [skills]
|
subgraph cat_skills [skills]
|
||||||
@@ -167,6 +171,12 @@ flowchart LR
|
|||||||
funnel --> subagent_model
|
funnel --> subagent_model
|
||||||
dialectic --> subagent_model
|
dialectic --> subagent_model
|
||||||
democracy --> merge_fix
|
democracy --> merge_fix
|
||||||
|
democracy -. claim and release .-> rasci
|
||||||
|
funnel --> rasci
|
||||||
|
status_matrix --> rasci
|
||||||
|
cleanup --> artefacts
|
||||||
|
artefacts --> rasci
|
||||||
|
dream --> rasci
|
||||||
|
|
||||||
observe --> triage
|
observe --> triage
|
||||||
observe --> ci_artifacts
|
observe --> ci_artifacts
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: kvb-check-principles
|
name: kvb-check-principles
|
||||||
description: >
|
description: >
|
||||||
Analyse a repository, path or diff with a hostile eye for violated
|
Analyse a repository, path or diff for violated principles: SPOT, DRY, KISS,
|
||||||
principles: SPOT, DRY, KISS, SRP, security at trust boundaries. Trigger on
|
SRP, security at trust boundaries. Trigger on /kvb-check-principles, "sei
|
||||||
/kvb-check-principles, "sei skeptisch", "kritische analyse", "review the architecture",
|
skeptisch", "kritische analyse", "review the architecture", or "what is
|
||||||
or "what is wrong with the code".
|
wrong with the code".
|
||||||
---
|
---
|
||||||
|
|
||||||
The stance is adversarial: the code is guilty until its structure proves
|
The stance is adversarial: the code is guilty until its structure proves
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ jobs' logs, and a first clustering. The judgement in those steps stays yours.
|
|||||||
3. **Coordinate every cluster.** Run the `kvb-org-organize` skill's Lineage and Peer
|
3. **Coordinate every cluster.** Run the `kvb-org-organize` skill's Lineage and Peer
|
||||||
protocol for each cluster before fixing it: claim it, ask the lineage
|
protocol for each cluster before fixing it: claim it, ask the lineage
|
||||||
whether someone already fixes it, and take their fix or keep yours. Keep
|
whether someone already fixes it, and take their fix or keep yours. Keep
|
||||||
investigating while you wait for answers.
|
investigating while you wait for answers. In the `kvb-org-rasci` register
|
||||||
|
the cluster's objective is the root cause fixed, its key results are the
|
||||||
|
failing jobs passing again, one per job.
|
||||||
4. **Download artifacts per cluster.** Use the `kvb-ci-artifacts` skill for the
|
4. **Download artifacts per cluster.** Use the `kvb-ci-artifacts` skill for the
|
||||||
mechanics — it covers per-job logs while a run is still in progress, which
|
mechanics — it covers per-job logs while a run is still in progress, which
|
||||||
bundle to pull, and the grep hygiene that keeps a false positive from
|
bundle to pull, and the grep hygiene that keeps a false positive from
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: kvb-cleanup
|
name: kvb-cleanup
|
||||||
description: >
|
description: >
|
||||||
Free disk space agents filled: ask peer sessions what they still need,
|
Free disk space agents filled: ask peers what they still need,
|
||||||
inventory /tmp, Docker and agent dirs, and delete only what the operator
|
inventory /tmp, Docker and agent dirs, and delete only what the operator
|
||||||
picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen", "platte voll", "no
|
picks. Trigger on /kvb-cleanup, "aufräumen", "bereinigen", "platte voll", "no
|
||||||
space left on device".
|
space left on device".
|
||||||
@@ -60,6 +60,9 @@ Read-only, in parallel, sizes included:
|
|||||||
gh, buildx), worktrees with `git status --short` and whether their branch is
|
gh, buildx), worktrees with `git status --short` and whether their branch is
|
||||||
merged or pushed, Ansible and build temp dirs. With `pkgmgr` installed,
|
merged or pushed, Ansible and build temp dirs. With `pkgmgr` installed,
|
||||||
merged branches and worktrees go to the `kvb-git-branch-gc` skill instead.
|
merged branches and worktrees go to the `kvb-git-branch-gc` skill instead.
|
||||||
|
- **Register**: `rasci.py artifacts` of the `kvb-org-artefacts` skill:
|
||||||
|
workdir, branch and artifacts of every task in the `kvb-org-rasci` register,
|
||||||
|
with its status and worker; `--ended` narrows it to the candidates.
|
||||||
- **Live use**: open handles (`fuser`, `lsof`) and running processes by name
|
- **Live use**: open handles (`fuser`, `lsof`) and running processes by name
|
||||||
only (`ps -o pid,etime,comm`). Never print full command lines or
|
only (`ps -o pid,etime,comm`). Never print full command lines or
|
||||||
environments: process arguments carry credentials. A sandboxed agent often
|
environments: process arguments carry credentials. A sandboxed agent often
|
||||||
@@ -81,12 +84,15 @@ worktree or compose project name, a container label, an open handle, or
|
|||||||
`unknown`. Then apply, first match wins:
|
`unknown`. Then apply, first match wins:
|
||||||
|
|
||||||
1. Claimed by a peer: `keep`.
|
1. Claimed by a peer: `keep`.
|
||||||
2. A job running on it (deploy, gate, test, swarm run, build): `keep
|
2. Listed in the register under a task that is not `done`, `stopped` or `stale`:
|
||||||
|
`keep (task <task>)`. Under an ended task it is a candidate whose owner is
|
||||||
|
the task's worker.
|
||||||
|
3. A job running on it (deploy, gate, test, swarm run, build): `keep
|
||||||
(running)`.
|
(running)`.
|
||||||
3. Attributable to a peer that has not answered: `keep (no answer from
|
4. Attributable to a peer that has not answered: `keep (no answer from
|
||||||
<peer>)`. Silence is not consent.
|
<peer>)`. Silence is not consent.
|
||||||
4. Archive duty: `archive first`.
|
5. Archive duty: `archive first`.
|
||||||
5. Everything else is a candidate with a risk class:
|
6. Everything else is a candidate with a risk class:
|
||||||
|
|
||||||
| Risk | Meaning | Typical items |
|
| Risk | Meaning | Typical items |
|
||||||
| ---- | ------- | ------------- |
|
| ---- | ------- | ------------- |
|
||||||
@@ -157,6 +163,8 @@ ticked items. Revise repeats call 1; nothing is deleted before `Execute`.
|
|||||||
- Idle running stacks go down through the project's teardown target, never by
|
- Idle running stacks go down through the project's teardown target, never by
|
||||||
removing their containers or volumes underneath them.
|
removing their containers or volumes underneath them.
|
||||||
- Archive-first items: copy out, verify the copy, then delete.
|
- Archive-first items: copy out, verify the copy, then delete.
|
||||||
|
- Drop every deleted item from its task as the `kvb-org-artefacts` skill
|
||||||
|
says (`set <task> --drop-artifact <kind>:<ref>`), so `artifacts` stays true.
|
||||||
- A delete failing on permissions (files owned by a container UID): use the
|
- A delete failing on permissions (files owned by a container UID): use the
|
||||||
project's container helper. Never sudo, never bypass the sandbox; without a
|
project's container helper. Never sudo, never bypass the sandbox; without a
|
||||||
helper, report the path.
|
helper, report the path.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name: kvb-code-test
|
name: kvb-code-test
|
||||||
description: >
|
description: >
|
||||||
Discover the Makefile's test targets, let the operator pick, run and report;
|
Discover the Makefile's test targets, let the operator pick, run and report;
|
||||||
fix mode loops until green. Trigger on /kvb-code-test, /kvb-code-test --fix, or when the
|
fix mode loops until green. Trigger on /kvb-code-test [--fix], or when the
|
||||||
operator asks to run the tests, make them pass, or get the suite green.
|
operator asks to run the tests, make them pass, or get the suite green.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
name: kvb-code-tune
|
name: kvb-code-tune
|
||||||
description: >
|
description: >
|
||||||
Find and apply performance optimizations (CI, deploy, build, gate, hot
|
Find and apply performance optimizations (CI, deploy, build, gate, hot
|
||||||
paths) without reducing logic, coverage or correctness. Trigger on /kvb-code-tune,
|
paths) without reducing logic, coverage or correctness. Trigger on
|
||||||
when a job dies on its time budget, when a pipeline or deploy grows slower,
|
/kvb-code-tune, a job dying on its time budget, a slower pipeline or deploy,
|
||||||
or asked where time goes.
|
or asked where time goes.
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -113,7 +113,9 @@ every agent session on this machine, not only the branch lineage.
|
|||||||
- **The first to identify a cluster works it.** "First" is the `kvb-org-organize`
|
- **The first to identify a cluster works it.** "First" is the `kvb-org-organize`
|
||||||
claim `cluster/<slug>`, whose `--work` names the cluster's log line and
|
claim `cluster/<slug>`, whose `--work` names the cluster's log line and
|
||||||
number; between machines, the earliest announcement by message time. A
|
number; between machines, the earliest announcement by message time. A
|
||||||
claim without a measurement does not count.
|
claim without a measurement does not count. In the `kvb-org-rasci` register
|
||||||
|
the cluster's key result is the measured target (`<step> from <before> to
|
||||||
|
under <after>`), met only by the re-measurement.
|
||||||
- **Someone was earlier:** send them your measurements and findings, then move
|
- **Someone was earlier:** send them your measurements and findings, then move
|
||||||
on to the next cluster. Never build a second optimization of the same
|
on to the next cluster. Never build a second optimization of the same
|
||||||
cluster.
|
cluster.
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
name: kvb-create-feature
|
name: kvb-create-feature
|
||||||
description: >
|
description: >
|
||||||
Derive missing work (principles pass for structural flaws, usability pass for
|
Derive missing work (principles pass for structural flaws, usability pass for
|
||||||
missing features) and write requirement documents after single-select
|
missing features) and write requirement documents after confirmation.
|
||||||
confirmation. Trigger on /kvb-create-feature, "was fehlt noch", "welche features
|
Trigger on /kvb-create-feature, "was fehlt noch", "welche features brauchen
|
||||||
brauchen wir", or when the operator asks what to build next.
|
wir", or asked what to build next.
|
||||||
---
|
---
|
||||||
|
|
||||||
Two blind spots produce different work: the code has defects of structure,
|
Two blind spots produce different work: the code has defects of structure,
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ verified, and an unverifiable plan cannot be handed to a robot.
|
|||||||
trade-off the plan rests on gets settled before execution, escalating to the
|
trade-off the plan rests on gets settled before execution, escalating to the
|
||||||
`kvb-think-dialectic` skill where being wrong is expensive. The robot does not ask, so
|
`kvb-think-dialectic` skill where being wrong is expensive. The robot does not ask, so
|
||||||
an unresolved decision becomes a guess at runtime.
|
an unresolved decision becomes a guess at runtime.
|
||||||
6. **Record the plan as a todo list.** Write the sub-objectives into the
|
6. **Record the plan in the register.** Enter the objective in the
|
||||||
harness's own todo tracking, one entry per item, phrased as the outcome. Keep
|
`kvb-org-rasci` register with each sub-objective's verification as a key
|
||||||
a plan file only when the operator asks for one.
|
result, and mirror the sub-objectives into the harness's own todo tracking,
|
||||||
|
one entry per item, phrased as the outcome. Keep a plan file only when the
|
||||||
|
operator asks for one.
|
||||||
7. **Present it and wait.** Show the objective, the ordered sub-objectives with
|
7. **Present it and wait.** Show the objective, the ordered sub-objectives with
|
||||||
their verifications, what is deliberately out of scope, and the assumptions
|
their verifications, what is deliberately out of scope, and the assumptions
|
||||||
the plan rests on. Stop here: the plan is the deliverable of this step.
|
the plan rests on. Stop here: the plan is the deliverable of this step.
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ Then ask with `AskUserQuestion`:
|
|||||||
(still in use elsewhere), or `failed: <why>`.
|
(still in use elsewhere), or `failed: <why>`.
|
||||||
Report a failure as it stands; never remove by hand what gc skipped or
|
Report a failure as it stands; never remove by hand what gc skipped or
|
||||||
refused.
|
refused.
|
||||||
|
4. For every `done` item, drop its worktree and branch from the task that
|
||||||
|
lists them in the `kvb-org-rasci` register (`rasci.py artifacts` names
|
||||||
|
it): `set <task> --drop-artifact worktree:<path>`, and `--branch ''` where
|
||||||
|
the branch is gone.
|
||||||
|
|
||||||
## Step 5: unmerged and dirty
|
## Step 5: unmerged and dirty
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: kvb-org-artefacts
|
||||||
|
description: >
|
||||||
|
Artifacts of the kvb-org-rasci register per task: images, volumes,
|
||||||
|
worktrees. Trigger on /kvb-org-artefacts, "artefakte".
|
||||||
|
---
|
||||||
|
|
||||||
|
Artifacts live in the same table as the tasks of the `kvb-org-rasci`
|
||||||
|
register: each one belongs to exactly one task, carries its kind, reference
|
||||||
|
and the time it was recorded, and takes status and worker from its task. The
|
||||||
|
script is the register's `${CLAUDE_SKILL_DIR}/../kvb-org-rasci/scripts/rasci.py`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rasci.py set <task> --artifact <kind>:<ref>
|
||||||
|
rasci.py set <task> --drop-artifact <kind>:<ref>
|
||||||
|
rasci.py artifacts [--ended] [--task <task>]
|
||||||
|
```
|
||||||
|
|
||||||
|
Kinds: `image:`, `container:`, `volume:`, `network:`, `stack:`, `worktree:`,
|
||||||
|
`dir:`, `log:`. `artifacts` lists each task's workdir and branch beside them.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Record an artifact the moment it exists, under the task that created it;
|
||||||
|
one left unrecorded is invisible to cleanup.
|
||||||
|
- `artifacts --ended` lists what belongs to `done`, `stopped` or `stale`
|
||||||
|
tasks: the cleanup candidates, owned by the task's worker. Everything of an
|
||||||
|
open task stays.
|
||||||
|
- Deleting goes through the `kvb-cleanup` skill, where the operator picks;
|
||||||
|
drop the artifact here once it is gone.
|
||||||
|
- The register's `show` counts each task's artifacts in its Artifacts column.
|
||||||
@@ -7,5 +7,6 @@ description: >
|
|||||||
---
|
---
|
||||||
|
|
||||||
Run the `kvb-org-organize` skill's **Lineage** and **Peer protocol** sections for the
|
Run the `kvb-org-organize` skill's **Lineage** and **Peer protocol** sections for the
|
||||||
failure or fix at hand. They hold the whole procedure; this skill only keeps
|
failure or fix at hand. They hold the whole procedure, including the entries
|
||||||
`/kvb-org-coordinate` working.
|
in the `kvb-org-rasci` register; this skill only keeps `/kvb-org-coordinate`
|
||||||
|
working.
|
||||||
@@ -15,7 +15,7 @@ decides what to act on after reading the matrix.
|
|||||||
Gather in parallel, skip any source the session does not have, and never invent
|
Gather in parallel, skip any source the session does not have, and never invent
|
||||||
a row for a source you could not read:
|
a row for a source you could not read:
|
||||||
|
|
||||||
1. **Tasks** — the session's todo list, the operator's stated goals this
|
1. **Tasks** — the `kvb-org-rasci` register first, then the session's todo list, the operator's stated goals this
|
||||||
session, and any requirement or plan documents the work follows.
|
session, and any requirement or plan documents the work follows.
|
||||||
2. **Working tree** — `git status --short` and `git log --oneline -10`:
|
2. **Working tree** — `git status --short` and `git log --oneline -10`:
|
||||||
uncommitted files are open work, commits since the branch point are closed
|
uncommitted files are open work, commits since the branch point are closed
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ directory with `--session <id>`.
|
|||||||
worktree>"`. Exit 0: elected, or already yours. Exit 3: a live agent holds
|
worktree>"`. Exit 0: elected, or already yours. Exit 3: a live agent holds
|
||||||
it, and its claim is printed. Exit 4: the holder's session is dead (see
|
it, and its claim is printed. Exit 4: the holder's session is dead (see
|
||||||
Succession).
|
Succession).
|
||||||
4. **Elected**: continue with the Peer protocol; its message is the
|
4. **Elected**: the claim entered the task in the `kvb-org-rasci` register,
|
||||||
announcement.
|
the organisation's single point of truth; complete it there (objective,
|
||||||
|
description, priority, roles), then continue with the Peer protocol; its
|
||||||
|
message is the announcement.
|
||||||
5. **Not elected**: send the holder your findings and stop on that work.
|
5. **Not elected**: send the holder your findings and stop on that work.
|
||||||
|
|
||||||
A Claude Code session cannot edit a file in a git worktree before it holds a
|
A Claude Code session cannot edit a file in a git worktree before it holds a
|
||||||
@@ -74,8 +76,9 @@ records the result.
|
|||||||
- Before starting anything that consumes shared capacity (gates, deploys,
|
- Before starting anything that consumes shared capacity (gates, deploys,
|
||||||
iteration loops, swarm or cloud resources, cleanup of shared data), an agent
|
iteration loops, swarm or cloud resources, cleanup of shared data), an agent
|
||||||
sends the chair what, where, for how long and its expected disk need, waits
|
sends the chair what, where, for how long and its expected disk need, waits
|
||||||
for the grant, and reports the release. The chair keeps one queue, answers
|
for the grant, and reports the release. The chair keeps one queue as the
|
||||||
every request with its position, enforces the limits the operator set,
|
priorities in the `kvb-org-rasci` register, answers every request with its
|
||||||
|
position, enforces the limits the operator set,
|
||||||
checks every grant with `watch.py admit --need-gb <N>` (or `--kind <slot>`;
|
checks every grant with `watch.py admit --need-gb <N>` (or `--kind <slot>`;
|
||||||
test kinds also need CPU and RAM below the configured limits).
|
test kinds also need CPU and RAM below the configured limits).
|
||||||
- A running job (deploy, gate, test, swarm run, build) is never cut short to
|
- A running job (deploy, gate, test, swarm run, build) is never cut short to
|
||||||
@@ -154,14 +157,18 @@ The one protocol for every exchange between agents; `kvb-org-coordinate`, `kvb-c
|
|||||||
evidence (failing line quoted literally, run id, measurement), the
|
evidence (failing line quoted literally, run id, measurement), the
|
||||||
suspected cause marked as a thesis, and the question "Already known or
|
suspected cause marked as a thesis, and the question "Already known or
|
||||||
fixed on your side? Reply with a SHA or branch, or 'none'." The first line
|
fixed on your side? Reply with a SHA or branch, or 'none'." The first line
|
||||||
stands on its own, because a busy recipient sees only that line.
|
stands on its own, because a busy recipient sees only that line. In the
|
||||||
|
`kvb-org-rasci` register the peers asked are C, the chair that grants
|
||||||
|
capacity is S.
|
||||||
4. **Wait up to 5 minutes** before building anything, investigating read-only
|
4. **Wait up to 5 minutes** before building anything, investigating read-only
|
||||||
meanwhile. A fix that arrives goes through the project's normal path
|
meanwhile. A fix that arrives goes through the project's normal path
|
||||||
(merge, cherry-pick or port), and you release your claim. Silence is
|
(merge, cherry-pick or port), and you release your claim. Silence is
|
||||||
neither a "no" nor a competing claim: after 5 minutes, proceed.
|
neither a "no" nor a competing claim: after 5 minutes, proceed.
|
||||||
5. **Report** to the same peers: commit, branch, what changed, the evidence it
|
5. **Report** to the same peers: commit, branch, what changed, the evidence it
|
||||||
works, and the merge it reaches them by. Report an abandoned or failed
|
works, and the merge it reaches them by. Report an abandoned or failed
|
||||||
attempt too. Then release the claim.
|
attempt too. In the register, enter the peers told as I and mark every key
|
||||||
|
result the evidence proves as met. Then release the claim: the task ends
|
||||||
|
`done`, or `stopped` while a key result is open.
|
||||||
|
|
||||||
- Never invent a peer's answer.
|
- Never invent a peer's answer.
|
||||||
- A peer's report is not evidence: read the SHA, log or artifact it names
|
- A peer's report is not evidence: read the SHA, log or artifact it names
|
||||||
@@ -199,3 +206,6 @@ On /kvb-org-organize, one table of the current offices from `council.py list`, s
|
|||||||
claims marked:
|
claims marked:
|
||||||
|
|
||||||
| Office | Holder | Since | Work | Grants or queue |
|
| Office | Holder | Since | Work | Grants or queue |
|
||||||
|
|
||||||
|
Who carries which task, with priority and status, is the `kvb-org-rasci`
|
||||||
|
register's `show`.
|
||||||
@@ -16,10 +16,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
import datetime
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -28,6 +30,7 @@ ROOT = Path(os.environ.get("AGENTS_COUNCIL_ROOT", "/tmp/agents-council"))
|
|||||||
HEARTBEAT_LIMIT = 3600
|
HEARTBEAT_LIMIT = 3600
|
||||||
OFFICE = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$")
|
OFFICE = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$")
|
||||||
CLAIM = "claim.json"
|
CLAIM = "claim.json"
|
||||||
|
RASCI = Path(__file__).resolve().parents[2] / "kvb-org-rasci" / "scripts" / "rasci.py"
|
||||||
|
|
||||||
|
|
||||||
def live_sessions() -> dict[str, str | None]:
|
def live_sessions() -> dict[str, str | None]:
|
||||||
@@ -83,7 +86,7 @@ def claims() -> list[tuple[str, dict]]:
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> None:
|
def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> dict:
|
||||||
claim = {
|
claim = {
|
||||||
"office": office,
|
"office": office,
|
||||||
"session": sid,
|
"session": sid,
|
||||||
@@ -94,6 +97,30 @@ def write_claim(path: Path, office: str, sid: str, runtime: str, work: str) -> N
|
|||||||
"work": work,
|
"work": work,
|
||||||
}
|
}
|
||||||
(path / CLAIM).write_text(json.dumps(claim), encoding="utf-8")
|
(path / CLAIM).write_text(json.dumps(claim), encoding="utf-8")
|
||||||
|
return claim
|
||||||
|
|
||||||
|
|
||||||
|
def here() -> dict[str, str | None]:
|
||||||
|
try:
|
||||||
|
head = subprocess.run(
|
||||||
|
["git", "rev-parse", "--abbrev-ref", "HEAD"], capture_output=True, text=True, check=False
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
head = None
|
||||||
|
branch = head.stdout.strip() if head and head.returncode == 0 else ""
|
||||||
|
return {"workdir": os.getcwd(), "branch": branch if branch not in ("", "HEAD") else None}
|
||||||
|
|
||||||
|
|
||||||
|
def register(office: str, holder: str, status: str, **extra: str | None) -> None:
|
||||||
|
if not office.startswith(("work/", "cluster/")):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
spec = importlib.util.spec_from_file_location("rasci", RASCI)
|
||||||
|
rasci = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(rasci)
|
||||||
|
rasci.track(office, holder, status, **extra)
|
||||||
|
except (OSError, SystemExit) as exc:
|
||||||
|
print(f"council: {office} is missing from the rasci register ({exc})", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def cmd_claim(args: argparse.Namespace) -> int:
|
def cmd_claim(args: argparse.Namespace) -> int:
|
||||||
@@ -105,12 +132,21 @@ def cmd_claim(args: argparse.Namespace) -> int:
|
|||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
claim = read_claim(path)
|
claim = read_claim(path)
|
||||||
if claim is not None and claim["session"] == sid:
|
if claim is not None and claim["session"] == sid:
|
||||||
write_claim(path, args.office, sid, runtime, args.work)
|
claim = write_claim(path, args.office, sid, runtime, args.work)
|
||||||
|
register(args.office, claim["name"] or sid, "running", description=args.work, **here())
|
||||||
print(f"held {args.office}")
|
print(f"held {args.office}")
|
||||||
return 0
|
return 0
|
||||||
print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
|
print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
|
||||||
return 4 if is_stale(claim, live_sessions()) else 3
|
return 4 if is_stale(claim, live_sessions()) else 3
|
||||||
write_claim(path, args.office, sid, runtime, args.work)
|
claim = write_claim(path, args.office, sid, runtime, args.work)
|
||||||
|
register(
|
||||||
|
args.office,
|
||||||
|
claim["name"] or sid,
|
||||||
|
"running",
|
||||||
|
description=args.work,
|
||||||
|
previous=getattr(args, "previous", None),
|
||||||
|
**here(),
|
||||||
|
)
|
||||||
print(f"elected {args.office}")
|
print(f"elected {args.office}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -121,6 +157,8 @@ def cmd_takeover(args: argparse.Namespace) -> int:
|
|||||||
if path.exists() and not is_stale(claim, live_sessions()):
|
if path.exists() and not is_stale(claim, live_sessions()):
|
||||||
print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
|
print(json.dumps(claim) if claim else f"{args.office}: claim in progress")
|
||||||
return 3
|
return 3
|
||||||
|
if claim is not None:
|
||||||
|
args.previous = claim["name"] or claim["session"]
|
||||||
# Two successors racing between this check and the rename can both win;
|
# Two successors racing between this check and the rename can both win;
|
||||||
# kvb-org-organize names exactly one successor per office to keep that window closed.
|
# kvb-org-organize names exactly one successor per office to keep that window closed.
|
||||||
if path.exists():
|
if path.exists():
|
||||||
@@ -145,6 +183,7 @@ def cmd_release(args: argparse.Namespace) -> int:
|
|||||||
print(json.dumps(claim))
|
print(json.dumps(claim))
|
||||||
return 3
|
return 3
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
register(args.office, claim["name"] or claim["session"], "done")
|
||||||
print(f"released {args.office}")
|
print(f"released {args.office}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ Fable 5.1 $10 / $50.
|
|||||||
2. Start the brief with the line `task-class: <class>`; the spawn hook logs
|
2. Start the brief with the line `task-class: <class>`; the spawn hook logs
|
||||||
it together with `model`, `effort` and the Agent `description`.
|
it together with `model`, `effort` and the Agent `description`.
|
||||||
3. Set `model` and `effort` from the row; for `judgement` set neither.
|
3. Set `model` and `effort` from the row; for `judgement` set neither.
|
||||||
|
4. Enter the spawn in the `kvb-org-rasci` register: the subagent as R and as
|
||||||
|
`--worker "subagent <type> (<model>, <effort>)"` of the task it serves.
|
||||||
|
|
||||||
- An agent type whose definition sets its own model (Explore, Plan, the
|
- An agent type whose definition sets its own model (Explore, Plan, the
|
||||||
cavecrew agents) keeps it; never pass `model` or `effort` for it.
|
cavecrew agents) keeps it; never pass `model` or `effort` for it.
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
name: kvb-org-rasci
|
||||||
|
description: >
|
||||||
|
RASCI/OKR register of tasks: roles, key results, worker, status.
|
||||||
|
Trigger on /kvb-org-rasci, "rasci", "raci", "zuständigkeiten".
|
||||||
|
---
|
||||||
|
|
||||||
|
The register is the single point of truth of the organisation: every agent
|
||||||
|
records here what it works on, and claims, queue, handover and reports run
|
||||||
|
over it. `${CLAUDE_SKILL_DIR}/scripts/rasci.py` keeps it in
|
||||||
|
`<council root>/rasci.json`. A `kvb-org-organize` claim enters its task by
|
||||||
|
itself (holder as R and A, `running`, start, workdir, branch); its release
|
||||||
|
sets `done` and the end, or `stopped` while a key result is still open.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rasci.py set <task> --title <t> --objective <o> --description <d> --prio <n> --status queued|running|blocked|done|stopped --worker "<agent | subagent <type> (<model>)>" --workdir <dir> --branch <b> --artifact <kind>:<ref> --tool <script or command> [--start <iso>] [--end <iso>] [--drop-artifact <a>] [--drop-tool <t>]
|
||||||
|
rasci.py kr <task> add "<measurable result>" | met <n> | open <n> | rm <n>
|
||||||
|
rasci.py role <task> "<party>" <letters>
|
||||||
|
rasci.py get <task>
|
||||||
|
rasci.py rm <task>
|
||||||
|
rasci.py show [--all]
|
||||||
|
rasci.py artifacts [--ended] [--task <task>]
|
||||||
|
rasci.py tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objective and key results
|
||||||
|
|
||||||
|
The objective says what the task is for; its key results are the measurable
|
||||||
|
outcomes that prove it reached (a test green, a run passing, a release
|
||||||
|
published). `done` is refused while one is open; a task ended short of them is
|
||||||
|
`stopped`. `show` puts both in the OKR column as `<objective> (<met>/<all> KR)`.
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
- **R Responsible**: does the task: the claim holder, or the subagent it runs.
|
||||||
|
- **A Accountable**: exactly one; takes the final decision and carries the
|
||||||
|
overall responsibility. The claim holder, unless the operator decides.
|
||||||
|
- **S Supportive**: actively helps carry it out: a peer's fix, the chair's
|
||||||
|
grant, a stack or runner.
|
||||||
|
- **C Consulted**: asked for input before acting: line representatives,
|
||||||
|
lineage owners, the operator on a pending decision.
|
||||||
|
- **I Informed**: kept up to date on progress or result, without a say.
|
||||||
|
|
||||||
|
`-` clears a party; the operator is the party `Operator`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- On start, `set` title, objective, description, priority and worker, `kr
|
||||||
|
add` every key result, and `role` every party beyond R and A; on each
|
||||||
|
change, update the status. The worker is the one session or subagent
|
||||||
|
executing the task right now.
|
||||||
|
- Artifacts share this table and follow the `kvb-org-artefacts` skill:
|
||||||
|
record each one when it appears, drop it once deleted. Add every script or
|
||||||
|
tool the task ran (`--tool`): `tools` ranks them for `kvb-skills-dream`.
|
||||||
|
- Priority: the operator's order first, then the chair's queue; the chair
|
||||||
|
writes it.
|
||||||
|
- Write only your own rows; another session's task belongs to that session.
|
||||||
|
- On /kvb-org-rasci print `show` unchanged; its `gaps:` line names tasks
|
||||||
|
without exactly one A, without R, or open without a key result. `get`
|
||||||
|
prints one task in full, key results numbered.
|
||||||
Executable
+414
@@ -0,0 +1,414 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Keep the RASCI register of the organisation's tasks in a local JSON file.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
AGENTS_COUNCIL_ROOT: council directory shared by every agent on the host;
|
||||||
|
the register is <root>/rasci.json, and `show` also lists every work/
|
||||||
|
and cluster/ claim of the council there. /tmp/agents-council unless set.
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 done, 2 usage error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import contextlib
|
||||||
|
import datetime
|
||||||
|
import fcntl
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(os.environ.get("AGENTS_COUNCIL_ROOT", "/tmp/agents-council"))
|
||||||
|
DB = ROOT / "rasci.json"
|
||||||
|
LOCK = ROOT / "rasci.lock"
|
||||||
|
COUNCIL = Path(__file__).resolve().parents[2] / "kvb-org-organize" / "scripts" / "council.py"
|
||||||
|
TASK = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*$")
|
||||||
|
LETTERS = re.compile(r"^(?!.*(.).*\1)[RASCI]+$")
|
||||||
|
ORDER = "RASCI"
|
||||||
|
STATUSES = ("queued", "running", "blocked", "done", "stopped")
|
||||||
|
ENDED = ("done", "stopped")
|
||||||
|
FIELDS = (
|
||||||
|
"title", "objective", "description", "prio", "status", "worker", "start", "end", "workdir",
|
||||||
|
"branch",
|
||||||
|
)
|
||||||
|
TEXT_FIELDS = ("title", "objective", "description", "worker", "workdir", "branch")
|
||||||
|
LIST_FIELDS = {"tools": "tool"}
|
||||||
|
ARTIFACT = re.compile(r"^[a-z]+:\S")
|
||||||
|
OPERATOR = "Operator"
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message: str) -> None:
|
||||||
|
print(f"rasci: {message}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def check_task(task: str) -> None:
|
||||||
|
if not TASK.match(task):
|
||||||
|
fail(f"invalid task {task!r}; use a slug like work/<slug>")
|
||||||
|
|
||||||
|
|
||||||
|
def stamp(value: str = "now") -> str:
|
||||||
|
if value == "now":
|
||||||
|
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
|
||||||
|
try:
|
||||||
|
parsed = datetime.datetime.fromisoformat(value)
|
||||||
|
except ValueError:
|
||||||
|
fail(f"invalid time {value!r}; use ISO 8601 or now")
|
||||||
|
if parsed.tzinfo is None:
|
||||||
|
parsed = parsed.astimezone()
|
||||||
|
return parsed.astimezone(datetime.UTC).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
def letters_of(*parts: str) -> str:
|
||||||
|
return "".join(letter for letter in ORDER if any(letter in part for part in parts))
|
||||||
|
|
||||||
|
|
||||||
|
def load() -> dict[str, dict]:
|
||||||
|
try:
|
||||||
|
return json.loads(DB.read_text(encoding="utf-8"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
except ValueError:
|
||||||
|
fail(f"{DB} is not valid JSON")
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def database() -> Iterator[dict[str, dict]]:
|
||||||
|
ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
|
with LOCK.open("a") as lock:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||||
|
tasks = load()
|
||||||
|
yield tasks
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w", dir=ROOT, prefix=".rasci-", delete=False, encoding="utf-8"
|
||||||
|
) as tmp:
|
||||||
|
json.dump(tasks, tmp, indent=1, sort_keys=True)
|
||||||
|
os.replace(tmp.name, DB)
|
||||||
|
|
||||||
|
|
||||||
|
def open_results(entry: dict) -> int:
|
||||||
|
return sum(not kr["met"] for kr in entry.get("key_results", []))
|
||||||
|
|
||||||
|
|
||||||
|
def apply_status(entry: dict, status: str) -> None:
|
||||||
|
if status not in ENDED:
|
||||||
|
entry.pop("end", None)
|
||||||
|
entry["status"] = status
|
||||||
|
if status == "running":
|
||||||
|
entry.setdefault("start", stamp())
|
||||||
|
if status in ENDED:
|
||||||
|
entry.setdefault("end", stamp())
|
||||||
|
|
||||||
|
|
||||||
|
def track(task: str, holder: str, status: str, *, description: str | None = None,
|
||||||
|
previous: str | None = None, workdir: str | None = None,
|
||||||
|
branch: str | None = None) -> None:
|
||||||
|
"""Write a council claim (status running) or release (status done) through.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task: the claimed office, e.g. work/<slug>.
|
||||||
|
holder: the session holding the claim.
|
||||||
|
status: running on claim and takeover, done on release; a release with
|
||||||
|
open key results ends as stopped.
|
||||||
|
description: the claim's --work line, kept unless one is recorded.
|
||||||
|
previous: the stale holder a takeover replaces; loses its R and A.
|
||||||
|
workdir: the claiming process's directory, kept unless one is recorded.
|
||||||
|
branch: the checked-out branch there, kept unless one is recorded.
|
||||||
|
"""
|
||||||
|
with database() as tasks:
|
||||||
|
entry = tasks.setdefault(task, {"roles": {}})
|
||||||
|
roles = entry.setdefault("roles", {})
|
||||||
|
if previous and previous != holder:
|
||||||
|
left = roles.pop(previous, "").replace("R", "").replace("A", "")
|
||||||
|
if left:
|
||||||
|
roles[previous] = left
|
||||||
|
if entry.get("worker") == previous:
|
||||||
|
entry["worker"] = holder
|
||||||
|
entry.setdefault("worker", holder)
|
||||||
|
for field, value in (("description", description), ("workdir", workdir), ("branch", branch)):
|
||||||
|
if value:
|
||||||
|
entry.setdefault(field, value)
|
||||||
|
if status == "done" and open_results(entry):
|
||||||
|
status = "stopped"
|
||||||
|
if status in ENDED or entry.get("status") in (None, "queued", *ENDED):
|
||||||
|
apply_status(entry, status)
|
||||||
|
if status not in ENDED:
|
||||||
|
accountable = any("A" in v for p, v in roles.items() if p != holder)
|
||||||
|
roles[holder] = letters_of(roles.get(holder, ""), "R" if accountable else "RA")
|
||||||
|
|
||||||
|
|
||||||
|
def claimed() -> dict[str, dict]:
|
||||||
|
if not COUNCIL.is_file():
|
||||||
|
return {}
|
||||||
|
spec = importlib.util.spec_from_file_location("council", COUNCIL)
|
||||||
|
council = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(council)
|
||||||
|
live = council.live_sessions()
|
||||||
|
rows = {}
|
||||||
|
for office, claim in council.claims():
|
||||||
|
if not office.startswith(("work/", "cluster/")):
|
||||||
|
continue
|
||||||
|
holder = claim["name"] or claim["session"]
|
||||||
|
rows[office] = {
|
||||||
|
"description": claim["work"],
|
||||||
|
"status": "claimed",
|
||||||
|
"worker": holder,
|
||||||
|
"start": claim["since"],
|
||||||
|
"roles": {holder: "RA"},
|
||||||
|
"stale": council.is_stale(claim, live),
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def merged(include_done: bool) -> dict[str, dict]:
|
||||||
|
rows = claimed()
|
||||||
|
for task, entry in load().items():
|
||||||
|
base = rows.get(task, {"status": "queued", "roles": {}})
|
||||||
|
fields = {k: v for k, v in entry.items() if k != "roles"}
|
||||||
|
rows[task] = {**base, **fields, "roles": {**base["roles"], **entry.get("roles", {})}}
|
||||||
|
for row in rows.values():
|
||||||
|
if row.get("stale"):
|
||||||
|
row["status"] = "stale"
|
||||||
|
return {
|
||||||
|
task: row for task, row in rows.items() if include_done or row["status"] not in ENDED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cell(text: object, width: int) -> str:
|
||||||
|
text = " ".join(str(text).split()).replace("|", "\\|")
|
||||||
|
return text if len(text) <= width else text[: width - 1] + "…"
|
||||||
|
|
||||||
|
|
||||||
|
def when(value: str | None) -> str:
|
||||||
|
if not value:
|
||||||
|
return "-"
|
||||||
|
return datetime.datetime.fromisoformat(value).astimezone().strftime("%m-%d %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def gaps(task: str, row: dict) -> list[str]:
|
||||||
|
found = []
|
||||||
|
roles = row["roles"]
|
||||||
|
accountable = sum("A" in letters for letters in roles.values())
|
||||||
|
if accountable != 1:
|
||||||
|
found.append(f"{task} has {accountable} A")
|
||||||
|
if not any("R" in letters for letters in roles.values()):
|
||||||
|
found.append(f"{task} has no R")
|
||||||
|
if not row.get("key_results") and row["status"] not in ENDED:
|
||||||
|
found.append(f"{task} has no key result")
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_set(args: argparse.Namespace) -> int:
|
||||||
|
check_task(args.task)
|
||||||
|
if args.prio is not None and args.prio < 1:
|
||||||
|
fail("--prio must be 1 or higher")
|
||||||
|
times = {f: stamp(getattr(args, f)) for f in ("start", "end") if getattr(args, f)}
|
||||||
|
for ref in args.artifact:
|
||||||
|
if not ARTIFACT.match(ref):
|
||||||
|
fail(f"invalid artifact {ref!r}; use <kind>:<ref>, e.g. image:<name>:<tag>")
|
||||||
|
with database() as tasks:
|
||||||
|
entry = tasks.setdefault(args.task, {"roles": {}})
|
||||||
|
if args.status == "done" and open_results(entry):
|
||||||
|
fail(f"{args.task}: {open_results(entry)} key result(s) open; mark them met or set stopped")
|
||||||
|
for field in (*TEXT_FIELDS, "prio"):
|
||||||
|
value = getattr(args, field)
|
||||||
|
if value is not None:
|
||||||
|
entry[field] = value
|
||||||
|
if args.status:
|
||||||
|
apply_status(entry, args.status)
|
||||||
|
entry.update(times)
|
||||||
|
for field, flag in LIST_FIELDS.items():
|
||||||
|
drop = getattr(args, f"drop_{flag}")
|
||||||
|
items = [i for i in entry.get(field, []) if i not in drop]
|
||||||
|
items += [i for i in dict.fromkeys(getattr(args, flag)) if i not in items]
|
||||||
|
if items:
|
||||||
|
entry[field] = items
|
||||||
|
else:
|
||||||
|
entry.pop(field, None)
|
||||||
|
kept = [a for a in entry.get("artifacts", []) if a["ref"] not in args.drop_artifact]
|
||||||
|
known = {a["ref"] for a in kept}
|
||||||
|
kept += [{"ref": r, "added": stamp()} for r in dict.fromkeys(args.artifact) if r not in known]
|
||||||
|
if kept:
|
||||||
|
entry["artifacts"] = kept
|
||||||
|
else:
|
||||||
|
entry.pop("artifacts", None)
|
||||||
|
print(f"set {args.task}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_role(args: argparse.Namespace) -> int:
|
||||||
|
check_task(args.task)
|
||||||
|
letters = args.letters.upper()
|
||||||
|
if letters != "-" and not LETTERS.match(letters):
|
||||||
|
fail("letters must be distinct ones of R, A, S, C, I, or - to clear")
|
||||||
|
with database() as tasks:
|
||||||
|
roles = tasks.setdefault(args.task, {"roles": {}}).setdefault("roles", {})
|
||||||
|
if letters == "-":
|
||||||
|
roles.pop(args.party, None)
|
||||||
|
else:
|
||||||
|
roles[args.party] = letters_of(letters)
|
||||||
|
print(f"{args.task}: {args.party} {letters}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_kr(args: argparse.Namespace) -> int:
|
||||||
|
check_task(args.task)
|
||||||
|
with database() as tasks:
|
||||||
|
entry = tasks.setdefault(args.task, {"roles": {}})
|
||||||
|
results = entry.setdefault("key_results", [])
|
||||||
|
if args.action == "add":
|
||||||
|
if all(kr["text"] != args.value for kr in results):
|
||||||
|
results.append({"text": args.value, "met": False})
|
||||||
|
else:
|
||||||
|
if not args.value.isdigit() or not 1 <= int(args.value) <= len(results):
|
||||||
|
fail(f"{args.task}: no key result {args.value!r}; `get` lists them by number")
|
||||||
|
index = int(args.value) - 1
|
||||||
|
if args.action == "rm":
|
||||||
|
results.pop(index)
|
||||||
|
else:
|
||||||
|
results[index]["met"] = args.action == "met"
|
||||||
|
if not results:
|
||||||
|
entry.pop("key_results")
|
||||||
|
print(f"{args.task}: key result {args.action} {args.value}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get(args: argparse.Namespace) -> int:
|
||||||
|
row = merged(True).get(args.task)
|
||||||
|
if row is None:
|
||||||
|
fail(f"{args.task}: not in the register")
|
||||||
|
print(f"task: {args.task}")
|
||||||
|
for field in FIELDS:
|
||||||
|
if row.get(field) not in (None, ""):
|
||||||
|
print(f"{field}: {row[field]}")
|
||||||
|
for number, kr in enumerate(row.get("key_results", []), 1):
|
||||||
|
print(f"key result {number} [{'x' if kr['met'] else ' '}]: {kr['text']}")
|
||||||
|
for artifact in row.get("artifacts", []):
|
||||||
|
print(f"artifact: {artifact['ref']} (since {when(artifact['added'])})")
|
||||||
|
for field, flag in LIST_FIELDS.items():
|
||||||
|
for item in row.get(field, []):
|
||||||
|
print(f"{flag}: {item}")
|
||||||
|
roles = ", ".join(f"{p}={v}" for p, v in sorted(row["roles"].items()))
|
||||||
|
print(f"roles: {roles or '-'}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_artifacts(args: argparse.Namespace) -> int:
|
||||||
|
rows = merged(True)
|
||||||
|
for task in sorted(rows):
|
||||||
|
row = rows[task]
|
||||||
|
if args.task and task != args.task:
|
||||||
|
continue
|
||||||
|
if args.ended and row["status"] not in (*ENDED, "stale"):
|
||||||
|
continue
|
||||||
|
places = [(f"{f}:{row[f]}", None) for f in ("workdir", "branch") if row.get(f)]
|
||||||
|
recorded = [(a["ref"], a["added"]) for a in row.get("artifacts", [])]
|
||||||
|
for ref, added in (*places, *recorded):
|
||||||
|
print(f"{row['status']}\t{task}\t{row.get('worker') or '-'}\t{ref}\t{when(added)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_tools(_: argparse.Namespace) -> int:
|
||||||
|
used: dict[str, list[str]] = {}
|
||||||
|
for task, row in merged(True).items():
|
||||||
|
for tool in row.get("tools", []):
|
||||||
|
used.setdefault(tool, []).append(task)
|
||||||
|
for tool, tasks in sorted(used.items(), key=lambda item: (-len(item[1]), item[0])):
|
||||||
|
print(f"{len(tasks)}\t{tool}\t{' '.join(sorted(tasks))}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_rm(args: argparse.Namespace) -> int:
|
||||||
|
check_task(args.task)
|
||||||
|
with database() as tasks:
|
||||||
|
removed = tasks.pop(args.task, None)
|
||||||
|
print(f"removed {args.task}" if removed is not None else f"{args.task}: not in the register")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_show(args: argparse.Namespace) -> int:
|
||||||
|
rows = merged(args.all)
|
||||||
|
if not rows:
|
||||||
|
print("no tasks")
|
||||||
|
return 0
|
||||||
|
order = sorted(rows, key=lambda t: (rows[t].get("prio") is None, rows[t].get("prio") or 0, t))
|
||||||
|
header = ["Prio", "Task", "OKR", "Status", "Worker", "Artifacts", "Start", "End", *ORDER]
|
||||||
|
print("| " + " | ".join(header) + " |")
|
||||||
|
print("|" + "---|" * len(header))
|
||||||
|
problems = []
|
||||||
|
for task in order:
|
||||||
|
row = rows[task]
|
||||||
|
title = f"{task}: {row['title']}" if row.get("title") else task
|
||||||
|
results = row.get("key_results", [])
|
||||||
|
okr = cell(row.get("objective") or "-", 40)
|
||||||
|
if results:
|
||||||
|
okr += f" ({sum(kr['met'] for kr in results)}/{len(results)} KR)"
|
||||||
|
values = [
|
||||||
|
row.get("prio") or "-",
|
||||||
|
cell(title, 50),
|
||||||
|
okr,
|
||||||
|
row["status"],
|
||||||
|
cell(row.get("worker") or "-", 30),
|
||||||
|
len(row.get("artifacts", [])) or "-",
|
||||||
|
when(row.get("start")),
|
||||||
|
when(row.get("end")),
|
||||||
|
]
|
||||||
|
parties = sorted(row["roles"], key=lambda p: (p != OPERATOR, p.lower()))
|
||||||
|
values += [
|
||||||
|
cell(", ".join(p for p in parties if letter in row["roles"][p]), 30) for letter in ORDER
|
||||||
|
]
|
||||||
|
print("| " + " | ".join(str(v) for v in values) + " |")
|
||||||
|
problems += gaps(task, row)
|
||||||
|
if problems:
|
||||||
|
print("gaps: " + "; ".join(problems))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
p = sub.add_parser("set")
|
||||||
|
p.add_argument("task")
|
||||||
|
for field in (*TEXT_FIELDS, "start", "end"):
|
||||||
|
p.add_argument(f"--{field}")
|
||||||
|
p.add_argument("--prio", type=int)
|
||||||
|
for flag in (*LIST_FIELDS.values(), "artifact"):
|
||||||
|
p.add_argument(f"--{flag}", action="append", default=[])
|
||||||
|
p.add_argument(f"--drop-{flag}", action="append", default=[])
|
||||||
|
p.add_argument("--status", choices=STATUSES)
|
||||||
|
p.set_defaults(func=cmd_set)
|
||||||
|
p = sub.add_parser("kr")
|
||||||
|
p.add_argument("task")
|
||||||
|
p.add_argument("action", choices=("add", "met", "open", "rm"))
|
||||||
|
p.add_argument("value")
|
||||||
|
p.set_defaults(func=cmd_kr)
|
||||||
|
p = sub.add_parser("role")
|
||||||
|
p.add_argument("task")
|
||||||
|
p.add_argument("party")
|
||||||
|
p.add_argument("letters")
|
||||||
|
p.set_defaults(func=cmd_role)
|
||||||
|
for name, func in (("get", cmd_get), ("rm", cmd_rm)):
|
||||||
|
p = sub.add_parser(name)
|
||||||
|
p.add_argument("task")
|
||||||
|
p.set_defaults(func=func)
|
||||||
|
p = sub.add_parser("show")
|
||||||
|
p.add_argument("--all", action="store_true")
|
||||||
|
p.set_defaults(func=cmd_show)
|
||||||
|
p = sub.add_parser("artifacts")
|
||||||
|
p.add_argument("--ended", action="store_true")
|
||||||
|
p.add_argument("--task")
|
||||||
|
p.set_defaults(func=cmd_artifacts)
|
||||||
|
sub.add_parser("tools").set_defaults(func=cmd_tools)
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -28,6 +28,10 @@ Operate as an autonomous robot: given a goal, reach it without hand-holding.
|
|||||||
- **Verify, do not assume.** Reaching the goal means proving it: run it, read the
|
- **Verify, do not assume.** Reaching the goal means proving it: run it, read the
|
||||||
output, confirm the end state. Never report success on a step you did not
|
output, confirm the end state. Never report success on a step you did not
|
||||||
actually verify.
|
actually verify.
|
||||||
|
- **Run it over the register.** Before the first step, enter the goal in the
|
||||||
|
`kvb-org-rasci` register as the task's objective and every check that proves
|
||||||
|
it as a key result; mark each one met as its proof comes in. The run is
|
||||||
|
finished when the task can be set `done`.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ in a sidetree until it does, or leaves as a proposal.
|
|||||||
## The stages
|
## The stages
|
||||||
|
|
||||||
1. **Brief.** One concern per agent, on the model the `kvb-org-perform` skill
|
1. **Brief.** One concern per agent, on the model the `kvb-org-perform` skill
|
||||||
picks. Name the goal, the exact base commit, the
|
picks, entered in the `kvb-org-rasci` register as R with `--worker "subagent
|
||||||
|
<type> (<model>)"`. Name the goal, the exact base commit, the
|
||||||
scope that may change, the evidence expected back, a time cap (default
|
scope that may change, the evidence expected back, a time cap (default
|
||||||
60 minutes) at which it reports its interim state, and what the agent must
|
60 minutes) at which it reports its interim state, and what the agent must
|
||||||
not do: no pushes, no gates or deploys
|
not do: no pushes, no gates or deploys
|
||||||
@@ -35,7 +36,9 @@ in a sidetree until it does, or leaves as a proposal.
|
|||||||
behaviour, and makes a negative control: the test fails without the change.
|
behaviour, and makes a negative control: the test fails without the change.
|
||||||
Numbers come from the same source before and after.
|
Numbers come from the same source before and after.
|
||||||
4. **Self-assessment.** The agent reports fix confidence and break risk with
|
4. **Self-assessment.** The agent reports fix confidence and break risk with
|
||||||
their residuals and commits on its branch.
|
their residuals and commits on its branch. Its parent marks the key results
|
||||||
|
that evidence proves as met in the `kvb-org-rasci` register and records
|
||||||
|
the branch and worktree as `--branch` and `--artifact worktree:<path>`.
|
||||||
5. **Independent review.** Before anything is merged, run the `kvb-think-dialectic` skill
|
5. **Independent review.** Before anything is merged, run the `kvb-think-dialectic` skill
|
||||||
with skeptics who did not write the change. A hole they find returns the
|
with skeptics who did not write the change. A hole they find returns the
|
||||||
change to stage 3.
|
change to stage 3.
|
||||||
@@ -67,7 +70,8 @@ and verify each step instead of assuming it:
|
|||||||
only once every one of its commits is reachable from a pushed branch.
|
only once every one of its commits is reachable from a pushed branch.
|
||||||
6. **Release what it held**: slots (reported to the resource chair), port or
|
6. **Release what it held**: slots (reported to the resource chair), port or
|
||||||
subnet reservations, locks, its `kvb-org-organize` claims and every claim
|
subnet reservations, locks, its `kvb-org-organize` claims and every claim
|
||||||
announced to other agents.
|
announced to other agents. Drop each removed item from the register with
|
||||||
|
`--drop-artifact` and set the subagent's task `done` or `stopped`.
|
||||||
7. **Remove its temporary files**: logs, message files and scratch data that
|
7. **Remove its temporary files**: logs, message files and scratch data that
|
||||||
have served their purpose. Keep only the evidence the report cites, until
|
have served their purpose. Keep only the evidence the report cites, until
|
||||||
the report is delivered.
|
the report is delivered.
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: kvb-skills-auto
|
name: kvb-skills-auto
|
||||||
description: >
|
description: >
|
||||||
Turn operator repetition into a skill. Trigger when the operator gives the
|
Turn operator repetition into a skill. Trigger when the operator repeats an
|
||||||
same instruction, correction or prompt pattern about the third time in a
|
instruction, correction or prompt pattern a third time in a conversation, or
|
||||||
conversation, or says they keep repeating themselves, and the pattern is
|
says they keep repeating themselves, and the pattern is reusable.
|
||||||
reusable.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Its objective is the `kvb-skills-tune` skill's **Objective** section: a skill is worth
|
Its objective is the `kvb-skills-tune` skill's **Objective** section: a skill is worth
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ name: kvb-skills-dream
|
|||||||
description: >
|
description: >
|
||||||
Consolidate the last 24 hours of work in this repository: what held, what
|
Consolidate the last 24 hours of work in this repository: what held, what
|
||||||
broke, which hypotheses follow, into memory and improvement proposals.
|
broke, which hypotheses follow, into memory and improvement proposals.
|
||||||
Trigger on /kvb-skills-dream or when the operator asks to reflect on or learn from
|
Trigger on /kvb-skills-dream or a request to reflect on or learn from recent
|
||||||
recent work.
|
work.
|
||||||
---
|
---
|
||||||
|
|
||||||
Sleep on the work: replay what happened in this repository, separate what held
|
Sleep on the work: replay what happened in this repository, separate what held
|
||||||
@@ -48,6 +48,9 @@ Gather the window's actual record, not your recollection of it:
|
|||||||
logs of the red ones — the failing log line, not the job title.
|
logs of the red ones — the failing log line, not the job title.
|
||||||
- **Reviews**: pull requests and issues touched in the window, and the review
|
- **Reviews**: pull requests and issues touched in the window, and the review
|
||||||
comments on them.
|
comments on them.
|
||||||
|
- **Register**: the `kvb-org-rasci` register: `rasci.py tools` ranks the
|
||||||
|
scripts and tools the window's tasks ran, the refactoring candidates;
|
||||||
|
`get <task>` shows a task's objective, roles, workdir and artifacts.
|
||||||
- **Memory**: this project's memory directory and its `MEMORY.md` index — the
|
- **Memory**: this project's memory directory and its `MEMORY.md` index — the
|
||||||
findings already recorded, so the dream extends them instead of repeating them.
|
findings already recorded, so the dream extends them instead of repeating them.
|
||||||
- **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to
|
- **Instructions**: `CLAUDE.md`, `AGENTS.md`, and the project's own skills, to
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
name: kvb-think-criticize
|
name: kvb-think-criticize
|
||||||
description: >
|
description: >
|
||||||
Give constructive criticism of a problem, plan, design, decision, text or
|
Give constructive criticism of a problem, plan, design, decision, text or
|
||||||
code from diverse aspects, each point hardened by dialectic. Trigger on
|
code, each point hardened by dialectic. Trigger on /kvb-think-criticize,
|
||||||
/kvb-think-criticize, "kritisiere", "kritisch betrachten", "konstruktive Kritik", "was
|
"kritisiere", "kritisch betrachten", "konstruktive Kritik", "was haeltst du
|
||||||
haeltst du davon", or requests for honest feedback.
|
davon", or requests for honest feedback.
|
||||||
---
|
---
|
||||||
|
|
||||||
The stance is critical but constructive: the goal is a better result, not a
|
The stance is critical but constructive: the goal is a better result, not a
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
"""Exercise the kvb-org-rasci register: fields, roles, priority order, claims, artifacts, tools."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RASCI = REPO_ROOT / "skills" / "kvb-org-rasci" / "scripts" / "rasci.py"
|
||||||
|
COUNCIL = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "council.py"
|
||||||
|
HEADER = "| Prio | Task | OKR | Status | Worker | Artifacts | Start | End | R | A | S | C | I |"
|
||||||
|
|
||||||
|
|
||||||
|
def local(iso: str) -> str:
|
||||||
|
return datetime.datetime.fromisoformat(iso).astimezone().strftime("%m-%d %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
class TestRasci(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.tmp.cleanup)
|
||||||
|
self.home = Path(self.tmp.name) / "home"
|
||||||
|
self.root = Path(self.tmp.name) / "council"
|
||||||
|
self.sessions = self.home / ".claude" / "sessions"
|
||||||
|
self.sessions.mkdir(parents=True)
|
||||||
|
for sid, name in (("s1", "One"), ("s2", "Two")):
|
||||||
|
(self.sessions / f"{sid}.json").write_text(
|
||||||
|
json.dumps({"sessionId": sid, "name": name}), encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run(self, *args: str, script: Path = RASCI, sid: str = "s1") -> subprocess.CompletedProcess:
|
||||||
|
env = {
|
||||||
|
"PATH": "/usr/bin:/bin",
|
||||||
|
"HOME": str(self.home),
|
||||||
|
"AGENTS_COUNCIL_ROOT": str(self.root),
|
||||||
|
"CLAUDE_CODE_SESSION_ID": sid,
|
||||||
|
}
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(script), *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
cwd=self.tmp.name,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _out(self, *args: str) -> list[str]:
|
||||||
|
result = self._run(*args)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
return result.stdout.splitlines()
|
||||||
|
|
||||||
|
def _claim(self, office: str, session: str, name: str, work: str) -> None:
|
||||||
|
path = self.root / office
|
||||||
|
path.mkdir(parents=True)
|
||||||
|
claim = {
|
||||||
|
"office": office,
|
||||||
|
"session": session,
|
||||||
|
"runtime": "claude-code",
|
||||||
|
"name": name,
|
||||||
|
"since": "2026-11-01T00:00:00+00:00",
|
||||||
|
"beat": time.time(),
|
||||||
|
"work": work,
|
||||||
|
}
|
||||||
|
(path / "claim.json").write_text(json.dumps(claim), encoding="utf-8")
|
||||||
|
|
||||||
|
def test_matrix_orders_by_priority_and_lists_roles(self):
|
||||||
|
start = "2026-11-01T10:00:00+00:00"
|
||||||
|
self._run("set", "work/a", "--title", "Alpha", "--objective", "Ship A", "--prio", "2",
|
||||||
|
"--status", "running", "--worker", "Skills", "--start", start)
|
||||||
|
self._run("role", "work/a", "Skills", "ar")
|
||||||
|
self._run("role", "work/a", "Operator", "ic")
|
||||||
|
self._run("role", "work/a", "Pkgmgr", "C")
|
||||||
|
self._run("set", "work/b", "--title", "Beta", "--prio", "1", "--worker", "subagent Explore (haiku)")
|
||||||
|
self._run("role", "work/b", "Pkgmgr", "R")
|
||||||
|
lines = self._out("show")
|
||||||
|
self.assertEqual(lines[0], HEADER)
|
||||||
|
self.assertEqual(
|
||||||
|
lines[2], "| 1 | work/b: Beta | - | queued | subagent Explore (haiku) | - | - | - | Pkgmgr | | | | |"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
lines[3],
|
||||||
|
f"| 2 | work/a: Alpha | Ship A | running | Skills | - | {local(start)} | - "
|
||||||
|
"| Skills | Skills | | Operator, Pkgmgr | Operator |",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
lines[4], "gaps: work/b has 0 A; work/b has no key result; work/a has no key result"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_key_results_gate_done_and_fill_the_okr_column(self):
|
||||||
|
self._run("set", "work/a", "--objective", "Ship A", "--status", "running")
|
||||||
|
self._run("role", "work/a", "Skills", "RA")
|
||||||
|
self._run("kr", "work/a", "add", "tests green")
|
||||||
|
self._run("kr", "work/a", "add", "released")
|
||||||
|
self._run("kr", "work/a", "met", "1")
|
||||||
|
lines = self._out("show")
|
||||||
|
self.assertIn("| Ship A (1/2 KR) | running |", lines[2])
|
||||||
|
self.assertEqual(len(lines), 3)
|
||||||
|
self.assertEqual(self._run("set", "work/a", "--status", "done").returncode, 2)
|
||||||
|
self.assertIn("key result 2 [ ]: released", self._out("get", "work/a"))
|
||||||
|
self._run("kr", "work/a", "met", "2")
|
||||||
|
self.assertEqual(self._run("set", "work/a", "--status", "done").returncode, 0)
|
||||||
|
self.assertEqual(self._out("show"), ["no tasks"])
|
||||||
|
|
||||||
|
def test_status_sets_start_and_end(self):
|
||||||
|
self._run("set", "work/a", "--status", "running")
|
||||||
|
self._run("set", "work/a", "--status", "done", "--description", "long text")
|
||||||
|
lines = self._out("get", "work/a")
|
||||||
|
self.assertIn("status: done", lines)
|
||||||
|
self.assertIn("description: long text", lines)
|
||||||
|
self.assertTrue(any(line.startswith("start: 20") for line in lines))
|
||||||
|
self.assertTrue(any(line.startswith("end: 20") for line in lines))
|
||||||
|
self._run("set", "work/a", "--status", "running")
|
||||||
|
self.assertFalse(any(line.startswith("end:") for line in self._out("get", "work/a")))
|
||||||
|
|
||||||
|
def test_done_tasks_show_only_with_all(self):
|
||||||
|
self._run("set", "work/a", "--status", "done", "--end", "2026-11-01T12:00:00+00:00")
|
||||||
|
self._run("role", "work/a", "Skills", "RA")
|
||||||
|
self.assertEqual(self._out("show"), ["no tasks"])
|
||||||
|
self.assertIn("| done | - |", self._out("show", "--all")[2])
|
||||||
|
|
||||||
|
def test_artifacts_and_tools(self):
|
||||||
|
self._run("set", "work/a", "--workdir", "/w/a", "--branch", "feat/a", "--status", "done",
|
||||||
|
"--artifact", "image:core:dev", "--artifact", "volume:db",
|
||||||
|
"--tool", "council.py", "--tool", "make test")
|
||||||
|
self._run("set", "work/b", "--worker", "Two", "--tool", "make test", "--artifact", "dir:/tmp/b")
|
||||||
|
self._run("set", "work/a", "--drop-artifact", "volume:db")
|
||||||
|
self._run("set", "work/b", "--branch", "")
|
||||||
|
rows = [line.split("\t") for line in self._out("artifacts")]
|
||||||
|
self.assertEqual(
|
||||||
|
[row[:4] for row in rows],
|
||||||
|
[
|
||||||
|
["done", "work/a", "-", "workdir:/w/a"],
|
||||||
|
["done", "work/a", "-", "branch:feat/a"],
|
||||||
|
["done", "work/a", "-", "image:core:dev"],
|
||||||
|
["queued", "work/b", "Two", "dir:/tmp/b"],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertEqual(rows[0][4], "-")
|
||||||
|
self.assertRegex(rows[2][4], r"^\d\d-\d\d \d\d:\d\d$")
|
||||||
|
self.assertEqual([r.split("\t")[1] for r in self._out("artifacts", "--ended")], ["work/a"] * 3)
|
||||||
|
self.assertEqual(len(self._out("artifacts", "--task", "work/b")), 1)
|
||||||
|
self.assertIn("| queued | Two | 1 |", self._out("show")[2])
|
||||||
|
self.assertTrue(any(line.startswith("artifact: image:core:dev (since ") for line in self._out("get", "work/a")))
|
||||||
|
self.assertEqual(
|
||||||
|
self._out("tools"), ["2\tmake test\twork/a work/b", "1\tcouncil.py\twork/a"]
|
||||||
|
)
|
||||||
|
self.assertIn("tool: council.py", self._out("get", "work/a"))
|
||||||
|
|
||||||
|
def test_claims_join_the_matrix_with_holder_as_ra(self):
|
||||||
|
self._claim("work/live", "s1", "One", "Gamma")
|
||||||
|
self._claim("cluster/dead", "s9", "Gone", "Delta")
|
||||||
|
self._claim("chair", "s1", "One", "allocator")
|
||||||
|
self._run("set", "work/live", "--prio", "1", "--status", "running")
|
||||||
|
self._run("role", "work/live", "Two", "S")
|
||||||
|
lines = self._out("show")
|
||||||
|
self.assertTrue(lines[2].startswith("| 1 | work/live | - | running | One |"))
|
||||||
|
self.assertTrue(lines[2].endswith("| One | One | Two | | |"))
|
||||||
|
self.assertTrue(any(line.startswith("| - | cluster/dead | - | stale | Gone |") for line in lines))
|
||||||
|
self.assertFalse(any("chair" in line for line in lines))
|
||||||
|
|
||||||
|
def test_council_claim_takeover_and_release_write_through(self):
|
||||||
|
self.assertEqual(self._run("claim", "work/x", "--work", "Do X", script=COUNCIL).returncode, 0)
|
||||||
|
lines = self._out("get", "work/x")
|
||||||
|
self.assertIn("status: running", lines)
|
||||||
|
self.assertIn("worker: One", lines)
|
||||||
|
self.assertIn("description: Do X", lines)
|
||||||
|
self.assertIn(f"workdir: {self.tmp.name}", lines)
|
||||||
|
self.assertIn("roles: One=RA", lines)
|
||||||
|
(self.sessions / "s1.json").unlink()
|
||||||
|
self.assertEqual(self._run("takeover", "work/x", "--work", "Do X", script=COUNCIL, sid="s2").returncode, 0)
|
||||||
|
lines = self._out("get", "work/x")
|
||||||
|
self.assertIn("worker: Two", lines)
|
||||||
|
self.assertIn("roles: Two=RA", lines)
|
||||||
|
self._run("kr", "work/x", "add", "X shipped")
|
||||||
|
self.assertEqual(self._run("release", "work/x", script=COUNCIL, sid="s2").returncode, 0)
|
||||||
|
lines = self._out("get", "work/x")
|
||||||
|
self.assertIn("status: stopped", lines)
|
||||||
|
self.assertTrue(any(line.startswith("end: 20") for line in lines))
|
||||||
|
|
||||||
|
def test_rm_and_clear_a_party(self):
|
||||||
|
self._run("set", "work/a", "--title", "Alpha")
|
||||||
|
self._run("role", "work/a", "Skills", "RA")
|
||||||
|
self._run("role", "work/a", "Pkgmgr", "C")
|
||||||
|
self._run("role", "work/a", "Pkgmgr", "-")
|
||||||
|
self.assertTrue(self._out("show")[2].endswith("| Skills | Skills | | | |"))
|
||||||
|
self.assertEqual(self._out("rm", "work/a"), ["removed work/a"])
|
||||||
|
self.assertEqual(self._out("show"), ["no tasks"])
|
||||||
|
|
||||||
|
def test_invalid_input_exits_2(self):
|
||||||
|
for args in (
|
||||||
|
("role", "work/a", "Skills", "RR"),
|
||||||
|
("role", "work/a", "Skills", "Q"),
|
||||||
|
("set", "Bad Task"),
|
||||||
|
("set", "work/a", "--prio", "0"),
|
||||||
|
("set", "work/a", "--status", "finished"),
|
||||||
|
("set", "work/a", "--start", "yesterday"),
|
||||||
|
("get", "work/none"),
|
||||||
|
("kr", "work/a", "met", "1"),
|
||||||
|
("set", "work/a", "--artifact", "no-kind"),
|
||||||
|
):
|
||||||
|
with self.subTest(args=args):
|
||||||
|
self.assertEqual(self._run(*args).returncode, 2)
|
||||||
|
self.assertFalse((self.root / "rasci.json").exists())
|
||||||
|
|
||||||
|
def test_pipe_in_a_title_does_not_break_the_table(self):
|
||||||
|
self._run("set", "work/a", "--title", "a | b")
|
||||||
|
self._run("role", "work/a", "Skills", "RA")
|
||||||
|
self.assertTrue(self._out("show")[2].startswith("| - | work/a: a \\| b | - | queued |"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestOrgSkillsUseTheRegister(unittest.TestCase):
|
||||||
|
def test_every_org_skill_names_the_register(self):
|
||||||
|
for skill in sorted((REPO_ROOT / "skills").glob("kvb-org-*/SKILL.md")):
|
||||||
|
if skill.parent.name == "kvb-org-rasci":
|
||||||
|
continue
|
||||||
|
with self.subTest(skill=skill.parent.name):
|
||||||
|
self.assertIn("`kvb-org-rasci`", skill.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user