@mmerterden/multi-agent-pipeline 20.30.0 → 20.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +45 -0
  3. package/README.tr.md +59 -0
  4. package/docs/facts.json +3 -3
  5. package/install/_unattended-profile.mjs +61 -0
  6. package/install/catalog-history.json +1 -1
  7. package/install/claude.mjs +9 -2
  8. package/install/index.mjs +10 -1
  9. package/install/templates/claude-hooks.json +12 -1
  10. package/manifest.json +123 -82
  11. package/package.json +2 -1
  12. package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
  13. package/pipeline/commands/multi-agent/office-on/SKILL.md +8 -7
  14. package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
  15. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
  16. package/pipeline/commands/multi-agent/review/SKILL.md +4 -3
  17. package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
  18. package/pipeline/commands/multi-agent/sync/SKILL.md +12 -0
  19. package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
  20. package/pipeline/contract/CHANGELOG.md +46 -0
  21. package/pipeline/contract/README.md +22 -8
  22. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  23. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  24. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  25. package/pipeline/contract/fixtures/launch-plan.json +1 -1
  26. package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
  27. package/pipeline/contract/fixtures/runs-empty.json +1 -1
  28. package/pipeline/contract/fixtures/runs-failed.json +6 -1
  29. package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
  30. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
  31. package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
  32. package/pipeline/contract/fixtures/runs-running.json +6 -1
  33. package/pipeline/contract/manifest.json +153 -19
  34. package/pipeline/contract/types/index.d.ts +304 -6
  35. package/pipeline/lib/bg-launch.mjs +106 -0
  36. package/pipeline/lib/code-digest.mjs +49 -0
  37. package/pipeline/lib/credential-resolve.mjs +42 -2
  38. package/pipeline/multi-agent-refs/features/office-threat-model.md +24 -6
  39. package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
  40. package/pipeline/multi-agent-refs/help/en.md +2 -2
  41. package/pipeline/multi-agent-refs/help/tr.md +2 -2
  42. package/pipeline/multi-agent-refs/phases/modes.md +7 -7
  43. package/pipeline/multi-agent-refs/phases/phase-5-report.md +16 -10
  44. package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
  45. package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
  46. package/pipeline/office/NOTICE.md +4 -1
  47. package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
  48. package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
  49. package/pipeline/office/assets/COUNTER_SINK.png +0 -0
  50. package/pipeline/office/assets/DEVICE_STAND.png +0 -0
  51. package/pipeline/office/assets/FILING_CABINET.png +0 -0
  52. package/pipeline/office/assets/FRIDGE.png +0 -0
  53. package/pipeline/office/assets/MAILBOX.png +0 -0
  54. package/pipeline/office/assets/MOP_BUCKET.png +0 -0
  55. package/pipeline/office/assets/PRINTER.png +0 -0
  56. package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
  57. package/pipeline/office/assets/SERVER_RACK.png +0 -0
  58. package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
  59. package/pipeline/office/assets/TOILET_STALL.png +0 -0
  60. package/pipeline/office/assets/VENDING.png +0 -0
  61. package/pipeline/office/assets/WASHBASIN.png +0 -0
  62. package/pipeline/office/assets/WATER_COOLER.png +0 -0
  63. package/pipeline/office/index.html +3996 -884
  64. package/pipeline/office/toolkit.json +3 -0
  65. package/pipeline/schemas/activity.schema.json +188 -4
  66. package/pipeline/schemas/agent-state.schema.json +493 -115
  67. package/pipeline/schemas/contract-error.schema.json +12 -1
  68. package/pipeline/schemas/job-change.schema.json +72 -0
  69. package/pipeline/schemas/job-request.schema.json +38 -0
  70. package/pipeline/schemas/launch.json +4 -4
  71. package/pipeline/schemas/office-credentials.schema.json +71 -0
  72. package/pipeline/schemas/office-jobs.json +253 -0
  73. package/pipeline/schemas/office-jobs.schema.json +62 -0
  74. package/pipeline/schemas/office-memory.schema.json +105 -0
  75. package/pipeline/schemas/office-settings-request.schema.json +30 -0
  76. package/pipeline/schemas/office-settings.schema.json +67 -0
  77. package/pipeline/schemas/phases.json +10 -2
  78. package/pipeline/schemas/phases.schema.json +86 -19
  79. package/pipeline/schemas/prs.schema.json +5 -0
  80. package/pipeline/schemas/prune-logs-request.schema.json +26 -0
  81. package/pipeline/schemas/prune-logs.schema.json +52 -0
  82. package/pipeline/schemas/repo-change.schema.json +54 -0
  83. package/pipeline/schemas/repo-request.schema.json +29 -0
  84. package/pipeline/schemas/repos.schema.json +49 -1
  85. package/pipeline/schemas/runs-index.schema.json +257 -46
  86. package/pipeline/schemas/system-status.schema.json +111 -0
  87. package/pipeline/schemas/tools.schema.json +5 -0
  88. package/pipeline/schemas/unattended-profile.json +7 -2
  89. package/pipeline/scripts/_active-run.mjs +2 -1
  90. package/pipeline/scripts/_spawn-async.mjs +9 -5
  91. package/pipeline/scripts/activity-snapshot.mjs +219 -23
  92. package/pipeline/scripts/autopilot-runner.mjs +7 -5
  93. package/pipeline/scripts/contract-server.mjs +551 -25
  94. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
  95. package/pipeline/scripts/job-launch.mjs +477 -0
  96. package/pipeline/scripts/launch-request.mjs +94 -9
  97. package/pipeline/scripts/office-build.mjs +148 -11
  98. package/pipeline/scripts/office-control.mjs +34 -7
  99. package/pipeline/scripts/office-credentials.mjs +106 -0
  100. package/pipeline/scripts/office-memory.mjs +200 -0
  101. package/pipeline/scripts/office-prune.mjs +125 -0
  102. package/pipeline/scripts/office-settings.mjs +145 -0
  103. package/pipeline/scripts/phase-tracker.sh +12 -5
  104. package/pipeline/scripts/prune-logs.sh +53 -2
  105. package/pipeline/scripts/runs-index.mjs +62 -2
  106. package/pipeline/scripts/system-status.mjs +196 -0
  107. package/pipeline/scripts/task-mirror.mjs +150 -0
  108. package/pipeline/scripts/task-mirror.sh +20 -0
  109. package/pipeline/scripts/toolkit-tools.mjs +33 -7
  110. package/pipeline/scripts/transcript-index.mjs +4 -1
  111. package/pipeline/scripts/work-items.mjs +191 -22
  112. package/pipeline/skills/.skill-manifest.json +6 -6
  113. package/pipeline/skills/.skills-index.json +1 -1
  114. package/pipeline/skills/shared/README.md +1 -1
  115. package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +3 -4
  116. package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
  117. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -0
  118. package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
  119. package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
  120. package/pipeline/skills/skills-index.md +1 -1
@@ -1,5 +1,13 @@
1
1
  # Office routes: threat model
2
2
 
3
+ <!-- toc -->
4
+ - [Assets](#assets)
5
+ - [Attackers](#attackers)
6
+ - [Threats and controls](#threats-and-controls)
7
+ - [Residual risks](#residual-risks)
8
+ - [Later, with a remote carrier](#later-with-a-remote-carrier)
9
+ <!-- /toc -->
10
+
3
11
  The office routes of `scripts/contract-server.mjs` let a client watch the
4
12
  agents on this machine, read what they said, answer and steer runs, and change
5
13
  what runs here (autopilot, stacks, routines). A browser panel may be served at
@@ -20,6 +28,8 @@ not mitigated here.
20
28
  | Run state | `agent-state.json`, `tracker-state.json` | an answer or steer written here changes what a run does |
21
29
  | Machine configuration | autopilot `config.json` and launchd jobs, a repo's `.claude/settings.json`, routine commands | widening any of them changes what runs unattended |
22
30
  | PR verdicts | posted to GitHub or Bitbucket with the user's own credential | an approval in the user's name |
31
+ | Learning stores | `~/.claude/memory/multi-agent/<repo>/` and `~/.claude/knowledge/<repo>/` | statements a run wrote down, which can quote code or a pasted secret |
32
+ | Run logs | `~/.claude/logs/multi-agent/<project>/<task>/` | deleting the wrong folder loses a live run's state |
23
33
 
24
34
  ## Attackers
25
35
 
@@ -36,20 +46,28 @@ not mitigated here.
36
46
  |---|---|---|---|
37
47
  | T1 | DNS rebinding: a page on an attacker host resolves to 127.0.0.1 and calls the API under its own name | Every request, `/office` and the event stream included, must carry `Host: 127.0.0.1:<port>` or `localhost:<port>`; anything else is 403 before routing | `smoke-office-security.sh` T1, `test/office-routes.test.mjs` "403 under a rebound Host" |
38
48
  | T2 | CSRF: a page posts to a write route | Writes (POST, DELETE) accept no `Origin` but the server's own, even an origin allowed with `--allow-origin`; a foreign preflight is 403 and no CORS header is ever sent to a foreign page; writes that widen what runs here take a second request with a single-use, route-bound confirm token | `smoke-office-security.sh` T2, `office-routes.test.mjs` "a write from a foreign page is refused" |
39
- | T3 | Token theft or replay | The token is read from the `Authorization` header only (never a cookie or a query parameter), compared in constant time over digests, never written into a response, an error or a log line. It never appears in a URL: `office-on` opens the page with a one-time code in the fragment (10 seconds, one use, minted by the bearer holder at `POST /office/code` with no Origin), and the page trades it at `POST /office/exchange`, which accepts only the server's own Origin; a second use, an expired or an unknown code is 401. The code never reaches a command line, which every local user can read (`ps`): the browser is opened on a 0600 launcher page in the 0700 state directory that carries the code and forwards with `location.replace`, under a CSP that allows only its own script by hash; the launcher is removed after 15 seconds. An Origin header can be forged by a non-browser client, so the code's secrecy and its 10 second, single-use life are what protect the exchange. The page keeps the token in memory and strips the fragment. The server exits after 8 hours however busy it is, so a token cannot outlive a working day | `office-control.test.mjs` "the opener's argv carries no code", "the launcher page has a strict CSP", "the launcher is deleted"; `office-routes.test.mjs` "a panel code trades for the token once", "a code is refused from a foreign page", "an expired code is 401", "exits at its absolute lifetime"; `smoke-office-page.sh` "the code trades once" |
49
+ | T3 | Token theft or replay | The token is read from the `Authorization` header only (never a cookie or a query parameter), compared in constant time over digests, never written into a response, an error or a log line. It never appears in a URL: `office-on` opens the page with a one-time code in the fragment (10 seconds, one use, minted by the bearer holder at `POST /office/code` with no Origin), and the page trades it at `POST /office/exchange`, which accepts only the server's own Origin; a second use, an expired or an unknown code is 401. The code never reaches a command line, which every local user can read (`ps`): the browser is opened on a 0600 launcher page in the 0700 state directory that carries the code and forwards with `location.replace`, under a CSP that allows only its own script by hash; the launcher is removed after 15 seconds. An Origin header can be forged by a non-browser client, so the code's secrecy and its 10 second, single-use life are what protect the exchange. The page keeps the token in memory and strips the fragment. A server started by `contract-server.mjs` alone exits after 8 hours however busy it is; the panel's own server (`office-on`) runs until `office-off`, because a person keeps it open beside their work. Its token lives only in the server's memory, the page's memory and a 0600 file, is never on a command line or in a URL, and is replaced each time `office-on` starts a server | `office-control.test.mjs` "the opener's argv carries no code", "the launcher page has a strict CSP", "the launcher is deleted"; `office-routes.test.mjs` "a panel code trades for the token once", "a code is refused from a foreign page", "an expired code is 401", "exits at its absolute lifetime"; `smoke-office-page.sh` "the code trades once" |
40
50
  | T4 | Secret exposure through a response | Transcripts are read through `_transcript-tail.mjs`, which never parses api-request records and drops thinking blocks; every string passes the credential redactor and the scrubber; the finished office and chat documents are checked by the outbound gate; seat labels come from allow-listed fields (tool name, file basename, sidecar description), never a command line; credentials are read only by the service scripts and never returned | `smoke-office-privacy.sh`, `smoke-office-security.sh` T4, `test/office-activity.test.mjs` "carries no credential" |
41
51
  | T5 | Path traversal into arbitrary files | Session, agent, run, routine, tool and evidence ids are matched against safe character sets after decoding; transcripts are looked up by name under the projects root only; a screenshot must resolve (real path) inside the run's evidence directory; an analysis document named in run state is read only when its real path is inside the run's log directory, its artifacts or a worktree git lists under the state's projectRoot, and only when it is Markdown or HTML | `smoke-office-security.sh` T5, `test/office-work.test.mjs` "nothing outside its directory", "analysis documents are read only inside the run's roots", "a worktreePath counts only when git lists it" |
42
52
  | T6 | Oversized or malformed input | Bodies are capped at 64 KiB and validated against the route's schema; tool arguments are checked against the tool's inputSchema; launch input goes through `launch-request.mjs` validation only; chat paging is capped at 500 | `smoke-office-security.sh` T6, `office-work.test.mjs` "refuses an unknown tool, invalid arguments and an oversized body" |
43
- | T7 | Resource exhaustion | At most four event streams and 64 connections; a phone request has five seconds to send its body before any signature work; at most 256 confirm tokens, the oldest dropped first; service clients, the work list and autopilot control run as child processes, never blocking the event loop, one at a time per service (429 `busy` for a second one); one snapshot is computed per poll from bounded tail reads; the session list is cached for ten seconds; the server exits after its idle time, after 8 hours in any case, and on SIGTERM | `smoke-office-security.sh` T7; `office-routes.test.mjs` "connections past the cap are dropped", "never sends its body", "confirm tokens are capped"; `office-work.test.mjs` "a slow service read neither blocks other routes nor runs twice at once" |
53
+ | T7 | Resource exhaustion | At most four event streams and 64 connections; a phone request has five seconds to send its body before any signature work; at most 256 confirm tokens, the oldest dropped first; service clients, the work list and autopilot control run as child processes, never blocking the event loop, one at a time per service (429 `busy` for a second one); one snapshot is computed per poll from bounded tail reads; the session list is cached for ten seconds; a server started on its own exits after its idle time and after 8 hours in any case, the one `office-on` starts runs until `office-off`, and both stop on SIGTERM | `smoke-office-security.sh` T7; `office-routes.test.mjs` "connections past the cap are dropped", "never sends its body", "confirm tokens are capped"; `office-work.test.mjs` "a slow service read neither blocks other routes nor runs twice at once" |
44
54
  | T8 | XSS from hostile transcript or issue text | The panel renders every server string with `textContent` or canvas text; markdown is shown as plain text; server images only as `data:` URLs of image types; the office sprites are same-origin PNGs from `/office/assets/`, which answers only a plain file name (no separator or escape) whose real path is a PNG directly in the installed assets directory, and 404 otherwise; the page language the server stamps comes from a two-value set (`en`, `tr`), whatever the preferences file holds; the page's CSP allows only its per-response nonce for script and style, `connect-src 'self'`, no framing, no base URI, no form action | `smoke-office-page.sh`, `office-routes.test.mjs` "stamps the page language from outputLanguage", "serves a sprite and nothing else under /office/assets/", `office-build.test.mjs`, `smoke-office-runtime.sh` |
45
55
  | T9 | Clickjacking | `frame-ancestors 'none'` on the page and on every API response | `office-routes.test.mjs` "serves the page without a token, with a nonce CSP" |
46
56
  | T10 | An action nobody can account for | Every write leaves one line (who, route, step, status; never a body or token) in `office-actions.jsonl` | `office-routes.test.mjs` "every write leaves a line" |
47
- | T11 | The panel widening the machine silently | Autopilot on, stack changes and routine removal are tagged `local-only` and `high`, take two requests, and run the same scripts as the terminal commands | `smoke-office-routes.sh`, `smoke-office-single-source.sh` |
57
+ | T11 | The panel widening the machine silently | Autopilot on, stack changes, routine removal and adding or removing a launch repository are tagged `local-only` and `high`, take two requests, and run the same scripts as the terminal commands where one exists (removing a launch repository has none). A repository is added only as a git checkout that is neither the home directory nor the root; a suggestion is named by an opaque id and resolved again on the server, so a page never sends or sees a path it did not type | `smoke-office-routes.sh`, `smoke-office-single-source.sh`, `office-repos.test.mjs`, `smoke-office-ui.sh` |
48
58
  | T12 | A toolkit tool used as a reader of local files or services (a path, a `file://` or loopback URL, a shared browser page, an index of files) | Tool calls are denied by default: only a tool that declares readOnlyHint, is not destructive, is not in the `web` or `context` family and whose inputSchema takes no location (a property named for a path, URL, file, directory, output, project, root and similar, or a uri format, at any depth) is callable; everything else is 403 `tool-not-allowed` and never reaches the server. A `confirm` argument is refused, so a tool's own preview is never skipped. The toolkit child always runs with `MULTI_AGENT_UNATTENDED=1` and `MCP_TOOLKIT_URL_POLICY=strict`, applied after the registration's own environment. An `@latest` registration is pinned to a configured version or refused. The route is `local-only`, and its audit line names the tool and carries a SHA-256 of the arguments, never the arguments | `office-work.test.mjs` "refuses every tool outside the allowlist with 403 and never runs it", "refuses a confirm argument", "runs unattended with the strict URL policy", "pinned to a known version or refused", "the audit line names the tool and hashes the arguments" |
49
59
  | T13 | A steer instruction injected into an unattended run | Steering is tagged `launch` and `high` and takes two requests: the first shows the text, the run's phase and the unread instruction it replaces, and returns a token bound to that run and exactly that text; the second carries only the token. A finished run cannot be steered | `office-routes.test.mjs` "previews first, then queues on confirm", "a steer token is single use, bound to its run and cannot carry new text" |
50
60
  | T14 | A review verdict posted from a stale or unfinished review | A verdict is previewed only for a finished review whose every finding is accepted, rejected or deferred (409 `review-incomplete` otherwise); the confirm token is bound to the decision and a digest of the findings, and a confirmation after either changed is 409 `confirm-mismatch` | `office-work.test.mjs` "a running review or one with an untriaged finding cannot be previewed", "refused when the findings changed after the preview" |
51
61
  | T15 | A service read pointed at another host | A Confluence URL is accepted only on the configured Confluence host, and with no host configured only a page id is accepted (409 `host-not-allowed`), before the client script runs; Crashlytics takes only a Firebase console URL | `office-work.test.mjs` "a Confluence URL on another host is refused before the client runs", "only a page id is accepted" |
52
62
  | T16 | Machine paths leaking through errors, or `office-off` signalling a reused pid | Error bodies name no absolute path (the launchd refusal and the unreadable settings message say what failed, not where); `office-off` sends SIGTERM only when the recorded pid's token file still names it, its command is `contract-server.mjs` and its start time matches the token file's | `office-routes.test.mjs` "refused without naming its path", "reported without the plist path"; `smoke-office-control.sh` "off never signals a pid that is not its server" |
63
+ | T17 | A reload code minted for another page, or a stale server outliving an update | `POST /office/reload` and `GET /office/version` need the bearer token and refuse any Origin other than the panel's own (a request with no Origin, a terminal client, is accepted); the reload code is the same 10 second, single-use code `office-on` uses, so a page that already holds the token gains nothing it did not have. A server whose code changed on disk is reported stale and `office-on` replaces it instead of reusing it | `office-repos.test.mjs` "/office/version reports the installed build", "/office/reload mints a one-time code"; `smoke-office-ui.sh` |
64
+ | T18 | The panel starting work nobody asked for, or work wider than asked | `POST /v1/jobs` takes only a job named in `schemas/office-jobs.json`, each parameter checked by its kind (a pull request URL or `repo#N`, a Jira key, a run id the index knows, a branch name, a path that stays inside the repository, an enumerated value; a pipeline run's work item goes through the launch request rules). Two requests: the preview names the role, the repository, the exact prompt and what the job writes outside the machine (a branch and PR for a pipeline run; review and clarify jobs report locally, since agent-guard keeps GitHub writes to the runner), and the token is bound to that payload. At the confirmation the repository must still be allowed, the unattended profile installed, the workspace trusted, agent-guard.sh registered for every tool and the autopilot runner's sandbox check passing; the session runs `claude --bg` with no permission prompts under that profile, without a shell, and claude's own output stays in the server's log, not the page. The unattended profile denies reading the server's token directory, so a session it starts cannot pick up the bearer. A team job is refused while a run is live in the same repository, from the moment its session starts. Tagged `local-only`/`high`: no phone route reaches it. Every start is recorded under `logs/multi-agent/office/jobs/` and in `office-actions.jsonl` | `office-jobs.test.mjs`, `smoke-office-ui.sh` |
65
+ | T19 | The key cabinet leaking a credential, or the panel becoming a place to enter one | `GET /v1/credentials` runs `lib/credential-inventory.sh`, whose probe reads each credential through the credential store with its output discarded, so no value reaches the server process. The route keeps the logical name, a state and a probe verdict from fixed vocabularies and a capability line cut to 160 characters; anything else in the script's output is dropped. Tagged `local-only`: not even the state goes to a phone. There is no write route: renewing a credential stays in the terminal (`/multi-agent:setup`), so a secret never passes through the browser | `office-routes.test.mjs` "GET /v1/credentials names each credential and its verdict, never a value" |
66
+ | T20 | A learned statement carrying a secret or a machine path to the page | `GET /v1/memory` returns counts, the code graph's age and size, and the five newest learning statements per repository, each passed through the transcript redactor (provider credential rules, paths and addresses) and cut to 120 characters. Repository names are matched against a safe character set before any file is opened. Read only; `local-only` | `office-routes.test.mjs` "GET /v1/memory reports every shelf, redacted" |
67
+ | T21 | Machine details through the resource panel | `GET /v1/system` returns numbers only (cores, load, memory, free disk, process counts, log folder size) and two fixed warnings; no path, process argument, user or host name. The schema allows nothing else. The measurement runs as child processes off the event loop, concurrent requests share one measurement, and the log folder size is reused for a minute. Readable from a phone, since it names nothing | `office-routes.test.mjs` "GET /v1/system returns numbers only: no path, user or host name" |
68
+ | T22 | Old run logs deleted wider than asked, or a live run's log removed | `POST /v1/prune-logs` takes only an age from a fixed set (7, 30 or 90 days) and runs `prune-logs.sh`, the script `/multi-agent:prune-logs` runs: the dry run is the preview; the confirmed step lists the folders again, refuses with `changed` when they differ, and then prunes only the previewed list through `--only-from`, under a token bound to that age and list. The script never touches the audit trail, the metrics corpus or the task counters, refuses a log root that is `/`, the home folder or a git checkout, and spares a folder written in the last minutes and a run that can still be resumed. Tagged `local-only`/`high` | `office-routes.test.mjs` "POST /v1/prune-logs previews, then prunes only on the token" |
69
+ | T23 | Pull request listing exposing a token, or the native task mirror writing into another run | Each GitHub account gh is logged in to is asked with `gh auth token` for that host and login; the token travels in `GH_TOKEN` (and `GH_ENTERPRISE_TOKEN` for an Enterprise Server host) for the child process only, never on a command line, in a response or a log. Bitbucket Server is read over https only (a plain-http host is refused unless it is the loopback), redirects are not followed, and the token, read from the credential store with a deadline, travels in the `Authorization` header only; an unreachable host is a reason on that source, not a failure of the list. A redacted answer drops URLs and logins, numbering the accounts instead. The task-mirror hook (PostToolUse on TaskCreate and TaskUpdate) writes only into the run whose recorded session is the calling session, skips phase tiles, never prints, and exits 0 on any input it cannot read | `work-items-prs.test.mjs`, `task-mirror.test.mjs` |
70
+ | T24 | The panel changing a preference wider than the terminal would, or posting addresses to a phone | `POST /v1/settings` takes one of three settings and one of its two values, previews the change and runs, under a token bound to that setting and value, what `/multi-agent:language`, `/multi-agent:model` or `/multi-agent:route-on`/`route-off` runs; route on passes the saved strategy back so only the switch changes, nothing the panel sends can add rules or a free provider, and two changes never run at once. Tagged `local-only`/`high`. Report links: `channelResults` URLs must be https, travel only in the run record's `channelUrls` (local, dropped from a redacted answer) and are shown as text, never followed | `office-routes.test.mjs` "POST /v1/settings changes a switch only on the token, through its script", `runs-index.test.mjs` |
53
71
 
54
72
  ## Residual risks
55
73
 
@@ -64,9 +82,9 @@ not mitigated here.
64
82
  nothing local does.
65
83
  - **A page on the server's own origin** is trusted. Only the page this server
66
84
  serves can be there, and its CSP keeps other code out.
67
- - **Starting a session**: the server returns the launch plan and never starts
68
- a host process, so a panel without a local process to run the plan cannot
69
- start work by itself. That is a deliberate boundary of the contract.
85
+ - **Starting a session**: `POST /v1/launch` returns the launch plan for a
86
+ client to start. `POST /v1/jobs` starts a session itself, because the office
87
+ is that client; it is bounded by T18 and is never reachable from a phone.
70
88
  - **Host permission prompts** are not forwarded: the host records none where
71
89
  another process can read them (`GET /v1/permissions` says `available:
72
90
  false`).
@@ -566,7 +566,7 @@ protected paths (`~/.claude/**`, `~/.copilot/**`, `~/.codex/**`,
566
566
  `~/.config/gh`, `~/.config/git`, `~/.gitconfig`, `~/.npmrc`, `~/.netrc`,
567
567
  `~/.git-credentials`, `.git/hooks/**`, `.git/config`, `**/.github/workflows/**`),
568
568
  and the toolkit env `MCP_TOOLKIT_URL_POLICY=strict` and
569
- `MCP_TOOLKIT_INDEX_DENY=~/.ssh,~/.aws,~/.gnupg,~/Library/Keychains`.
569
+ `MCP_TOOLKIT_INDEX_DENY=~/.ssh,~/.aws,~/.gnupg,~/Library/Keychains,~/.claude/logs/multi-agent/contract-server,~/.claude/logs/multi-agent/office`.
570
570
 
571
571
  The runner hands the file to each child with `claude --settings`, so only the
572
572
  unattended session loads it. `~/.claude/settings.json` is the attended posture
@@ -86,9 +86,9 @@ Status & Resume:
86
86
  /multi-agent:autopilot-status Running, queued, awaiting an answer, today's PRs
87
87
  /multi-agent:autopilot-off Stop picking work up (running work finishes; --now stops it)
88
88
  /multi-agent:serve Contract server for a client: 127.0.0.1, token, stops when idle
89
- /multi-agent:office-on Open the office panel in your browser: agents at work, chats, ledger, runs
89
+ /multi-agent:office-on Open the office in your browser: live runs and agents, PRs, keys, memory, system, jobs; its orientation room explains the rest
90
90
  /multi-agent:office-status Is the panel running, its URL (never the token), its last actions
91
- /multi-agent:office-off Close the panel now (it also stops by itself when idle)
91
+ /multi-agent:office-off Stop the panel's server (it runs until you do)
92
92
 
93
93
  Post-Hoc & Side-Channel:
94
94
 
@@ -85,9 +85,9 @@ Status & Resume:
85
85
  /multi-agent:autopilot-status Koşan, sıradaki, cevap bekleyen, bugünkü PR'lar
86
86
  /multi-agent:autopilot-off Yeni madde alma (koşan iş biter; --now onu da durdurur)
87
87
  /multi-agent:serve İstemci için sözleşme sunucusu: 127.0.0.1, token, boşta durur
88
- /multi-agent:office-on Ofis panelini tarayıcıda aç: çalışan ajanlar, sohbetler, defter, koşular
88
+ /multi-agent:office-on Ofisi tarayıcıda aç: canlı koşular ve ajanlar, PR'lar, anahtarlar, hafıza, sistem, işler; gerisini oryantasyon odası anlatır
89
89
  /multi-agent:office-status Panel çalışıyor mu, adresi (token asla), son işlemleri
90
- /multi-agent:office-off Paneli hemen kapat (boşta kalınca kendisi de kapanır)
90
+ /multi-agent:office-off Panelin sunucusunu durdur (sen durdurana kadar çalışır)
91
91
 
92
92
  Post-Hoc & Side-Channel:
93
93
 
@@ -66,19 +66,19 @@ Full contract: `$HOME/.claude/multi-agent-refs/phases/phase-5-report.md` (Autopi
66
66
 
67
67
  ## Analysis Mode (`/multi-agent:analysis`)
68
68
 
69
- A different **output kind**, not a shorter pipeline. It produces the analysis document and stops; no code, no branch, no PR. The 6-phase contract holds: Phase 2 is skipped and Phases 1, 3 and 4 are reinterpreted.
69
+ A different **output kind**, not a shorter pipeline. It produces the analysis document and stops; no code, no branch, no PR. It runs the same six slots under its own step names (`phases.json` `modes.analysis.names`), so the tracker and the task widget show what it is doing.
70
70
 
71
71
  ```
72
- Phase 0: Init -> Phase 1: Plan (analysis) -> Phase 3: Review -> Phase 4: Publish -> Phase 5: Report
72
+ Phase 0: Intake -> Phase 1: Evidence -> Phase 2: Synthesis -> Phase 3: Review -> Phase 4: Publish -> Phase 5: Report
73
73
  ```
74
74
 
75
75
  | Phase | Analysis mode |
76
76
  |---|---|
77
- | 0 Init | Same intake, but **no worktree**: nothing is isolated because nothing is built, so `state.run.repoPath` is the repo itself |
78
- | 1 Plan | The engine (`multi-agent-refs/analysis/*`): evidence, synthesis, render, humanizer, `validate-analysis-doc.mjs`. **No task decomposition** - there is no code to plan |
79
- | 2 Dev | **SKIP** |
80
- | 3 Review | Document review plus the open questions still marked `Acik / Open` |
81
- | 4 Commit | **Publish** instead: Local file / Confluence / Jira. Locked 6 still forbids `git add` and `git commit` |
77
+ | 0 Intake | Same intake, but **no worktree**: nothing is isolated because nothing is built, so `state.run.repoPath` is the repo itself |
78
+ | 1 Evidence | Every declared source fetched in parallel, repo evidence, Code Connect index, conventions (`multi-agent-refs/analysis/evidence.md`) |
79
+ | 2 Synthesis | Pass A concept layer, the convention preview, Pass B per platform (`multi-agent-refs/analysis/synthesis.md`). **No task decomposition** - there is no code to plan |
80
+ | 3 Review | Render, the humanizer, `validate-analysis-doc.mjs`, the document review and the open questions still marked `Acik / Open` |
81
+ | 4 Publish | Local file / Confluence / Jira. Locked 6 still forbids `git add` and `git commit` |
82
82
  | 5 Report | Channels, same as every mode |
83
83
 
84
84
  **No `autopilot` variant.** Worktree isolation buys nothing when no code is written, and the intake, the Pass B convention preview and the open-question resolution are interactive by nature; a zero-interaction analysis would be a document nobody agreed to.
@@ -1,9 +1,9 @@
1
1
  ### Phase 5: Report
2
2
 
3
- > **TLDR** - Close the loop in two halves: **External delivery** delegated to `/multi-agent:channels` (multi-select kanal + content - always pauses in autopilot, 30-min timeout ends session). **Internal capture** stays inline: `agent-log.md` finalization, telemetry emission, knowledge base + memory update. Channels command is the single source of truth for Jira/Confluence/Wiki/PR-description delivery - full contract in `commands/multi-agent/channels/SKILL.md`.
3
+ > **TLDR** - Two halves. **External delivery** goes to `/multi-agent:channels`, the single source of truth for Jira, Confluence, wiki and PR-description delivery (channel and content multi-select; always pauses in autopilot, a 30-minute timeout ends the session; contract in `commands/multi-agent/channels/SKILL.md`). **Internal capture** stays inline: `agent-log.md`, telemetry, knowledge base and memory.
4
4
 
5
5
  <!-- progress-contract: applied -->
6
- Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md` - lines for channels dispatch entry, per-adapter result, agent-log finalization, telemetry emission, knowledge+memory write.
6
+ Progress per `$HOME/.claude/multi-agent-refs/progress-contract.md`: channels dispatch, each adapter's result, agent-log, telemetry, knowledge and memory.
7
7
 
8
8
  ## Sub-step Tracking (required)
9
9
 
@@ -96,6 +96,11 @@ Pre-ticks from `prefs.global.reportChannels` + `prefs.global.reportContent`. Gre
96
96
 
97
97
  Tracker: `phase-tracker.sh sub 5 1 "Channels dispatch" completed` (or `failed` if ALL adapters failed; `skipped` if user declined all).
98
98
 
99
+ For the office, record each adapter's `{status, url}` (https only) under `channelResults.<pr|jira|confluence|wiki|issue>`; `write-state.mjs` merges, Step 1.5 adds `issue`:
100
+
101
+ ```bash
102
+ printf '%s' '{"channelResults":{"pr":{"status":"posted","url":"<url>"},"confluence":{"status":"skipped"}}}' | node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE" >/dev/null
103
+ ```
99
104
  ---
100
105
 
101
106
  ## Step 1.5 - GitHub Issue channel (when tracker is a GitHub issue)
@@ -112,6 +117,7 @@ bash ~/.claude/scripts/phase-tracker.sh sub 5 1.5 "Issue comment" in_progress
112
117
  # unattended: skipped - the runner posts (features/unattended-security.md)
113
118
  gh issue comment "$ISSUE_NUMBER" --repo "$ORG/$REPO" --body-file /tmp/channels-${TASK_ID}-issue.md
114
119
  bash ~/.claude/scripts/phase-tracker.sh sub 5 1.5 "Issue comment" completed
120
+ printf '%s' '{"channelResults":{"issue":{"status":"posted","url":"<url>"}}}' | node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE" >/dev/null
115
121
 
116
122
  # 2. Sync the Progress flag table from agent-state.json. Idempotent - the
117
123
  # script no-ops if the body already matches.
@@ -150,7 +156,7 @@ Write `agent-log.md` with ALL sections:
150
156
 
151
157
  ## Plan Todo Rollup (when `prefs.global.planTodos.enabled`)
152
158
 
153
- Rendered via `$HOME/.claude/lib/plan-todos.sh list "$TASK_ID"`. Inline the resulting markdown checklist verbatim - one line per todo with status marker ([x] / [~] / [/] / [!]), ID, task text, and notes when present. Status summary one-liner ("4/6 done, 1 skipped, 1 failed") goes at the top from `plan-todos.sh status`.
159
+ Rendered via `$HOME/.claude/lib/plan-todos.sh list "$TASK_ID"` and inlined verbatim: one line per todo with its marker ([x] / [~] / [/] / [!]), ID, text and notes, under the one-line summary from `plan-todos.sh status` ("4/6 done, 1 skipped, 1 failed").
154
160
 
155
161
  Skipped sections: when `planTodos.enabled` is false or no `plan.todos[]` was emitted, the Plan Todo block is omitted (no empty header). Phase 2 task-by-task progress still appears in the Timeline section either way.
156
162
 
@@ -183,7 +189,7 @@ Skipped sections: when `planTodos.enabled` is false or no `plan.todos[]` was emi
183
189
 
184
190
  ## Repo Profile
185
191
 
186
- (from `node $HOME/.claude/scripts/repo-profile.mjs report --repo "$PROJECT_ROOT" --state "$STATE_FILE"` and `state.repoProfile`: the `action`, whether it was confirmed, the fields that were `derived` against those `confirmed` or `manual`, and every `ignored[]` field with its confidence and reason. Omitted when the run has no `repoProfile`.)
192
+ (from `node $HOME/.claude/scripts/repo-profile.mjs report --repo "$PROJECT_ROOT" --state "$STATE_FILE"` and `state.repoProfile`: the `action`, whether it was confirmed, `derived` fields against `confirmed` or `manual` ones, and each `ignored[]` field with confidence and reason; omitted without a `repoProfile`.)
187
193
 
188
194
  ## Channels Summary
189
195
 
@@ -243,14 +249,14 @@ This is independent of the channels-side `reportContent.costSummary` (which gate
243
249
  bash $HOME/.claude/scripts/capture-flush.sh --state "$STATE_FILE"
244
250
  ```
245
251
 
246
- Every phase boundary already made this call (`operations.md`), so by Phase 5 it usually writes 0 rows - and that is the point. Phase 5 is the last flush, not the only one: it is the phase a run is least likely to reach, and a run killed in Phase 2 keeps what it established.
252
+ Every phase boundary already made this call (`operations.md`), so here it usually writes 0 rows: Phase 5 is the last flush, not the only one, because it is the phase a run is least likely to reach.
247
253
 
248
254
  What it does, so this doc stays inspectable:
249
255
 
250
- - **Triage ingest** (`triage-memory.mjs ingest`). accepted/deferred/rejected rows into the per-repo corpus, so Phase 1 enrichment and Phase 3 prior-art recall them later. Idempotent. Reads the salvaged copy under `artifactsPath` FIRST: Phase 4 removes the worktree, and a worktree-first reader degrades silently for exactly the runs worth rescuing. JSONL at `~/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl`, per-repo, never cross-leaking. Off when `prefs.global.priorArtEnrichment.ingestOnComplete = false`.
251
- - **Ledger distill** (`learnings-ledger.mjs from-triage`). Rejected findings become durable `rejected-preference` entries so reviewers stop re-raising them. Idempotent. Safety: blocking-severity rejections are NOT distilled - a wrong rejection must never permanently suppress that class; `learnings-ledger.mjs forget` clears a stale one. JSONL beside the corpus; its brief replays into Phase 1 and Phase 3. On by default via `prefs.global.learningsLedger.enabled`.
256
+ - **Triage ingest** (`triage-memory.mjs ingest`): accepted, deferred and rejected rows into the per-repo corpus (`~/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl`, never cross-repo) that Phase 1 enrichment and Phase 3 prior-art recall. Idempotent. It reads the salvaged copy under `artifactsPath` first, because Phase 4 removes the worktree and a worktree-first reader silently loses the runs worth rescuing. Off when `prefs.global.priorArtEnrichment.ingestOnComplete = false`.
257
+ - **Ledger distill** (`learnings-ledger.mjs from-triage`): rejected findings become durable `rejected-preference` entries so reviewers stop re-raising them. Idempotent. A blocking-severity rejection is never distilled, so a wrong one cannot suppress its class for good; `learnings-ledger.mjs forget` clears a stale entry. JSONL beside the corpus, replayed into Phase 1 and Phase 3; on via `prefs.global.learningsLedger.enabled`.
252
258
 
253
- No model runs in the flush - it derives everything from `triage-output.json` plus `agent-state.json`, which is what lets a hook call it. The model-dependent parts of this phase (Step 3 knowledge capture, memory synthesis) stay here, because a hook cannot think.
259
+ No model runs in the flush: it derives everything from `triage-output.json` and `agent-state.json`, which is what lets a hook call it. The parts that need a model (Step 3 knowledge capture, memory synthesis) stay here.
254
260
 
255
261
  A durable architectural fact is a judgement call, so it stays here rather than in the flush:
256
262
 
@@ -303,13 +309,13 @@ Extract reusable knowledge from this task and **append** to `$HOME/.claude/knowl
303
309
 
304
310
  **First task** (no knowledge dir): `mkdir -p`, create initial architecture.md + patterns.md from Phase 1. Log: "Knowledge base initialized for {project}"
305
311
 
306
- **Code graph refresh:** when `prefs.global.codeGraph.enabled` and `prefs.global.codeGraph.autoRefresh` are both true, rebuild the graph and re-render `GRAPH_REPORT.md` - the branch just changed the code the Phase 1 graph described. Costs no API tokens, never fails the run. Commands: `$HOME/.claude/multi-agent-refs/features/code-graph.md`.
312
+ **Code graph refresh:** with `prefs.global.codeGraph.enabled` and `.autoRefresh` both true, rebuild the graph and `GRAPH_REPORT.md`, since the branch changed the code Phase 1 described. No API tokens, never fails the run; commands in `$HOME/.claude/multi-agent-refs/features/code-graph.md`.
307
313
 
308
314
  **Save preferences**: Update prefs with Phase 5 selections (via channels return: `reportChannels`, `reportContent`, `confluenceUrls` per-project LRU).
309
315
 
310
316
  **Per-repo memory synthesis (opt-in via `prefs.global.perRepoMemory`):**
311
317
 
312
- After the knowledge-capture pass, dispatch a scoped synthesis subagent (sonnet, 10 turns max) that reads the current run's `agent-log.md` + the existing MEMORY.md index, and returns 0-3 new memory entries as a JSON array. Each entry: `{type: "user|feedback|project|reference", slug, title, description, body}`. The orchestrator saves them all, in order:
318
+ After knowledge capture, a scoped synthesis subagent (sonnet, 10 turns max) reads this run's `agent-log.md` and the MEMORY.md index and returns 0-3 new entries as a JSON array of `{type: "user|feedback|project|reference", slug, title, description, body}`. The orchestrator saves them all, in order:
313
319
 
314
320
  ```bash
315
321
  bash $HOME/.claude/scripts/memory-save.sh "$PROJECT_ROOT" --from-json memory-synthesis.json
@@ -0,0 +1,37 @@
1
+ # Standalone review: its own tracker record
2
+
3
+ A standalone review (the command seeded a `review-` id, so no pipeline run
4
+ owns this session) registers itself, so its progress shows wherever runs
5
+ are read (the office panel, `/multi-agent:status`). Inside a pipeline run the
6
+ run's own tracker already covers it; skip this block.
7
+
8
+ ```bash
9
+ case "$TASK_ID" in review-*)
10
+ S="${LOGS_ROOT:-$HOME/.claude/logs/multi-agent}/$TASK_ID"; mkdir -p "$S"
11
+ printf '{"taskId":"%s","mode":"review","status":"in_progress","currentPhase":0}' "$TASK_ID" \
12
+ | node "$HOME/.claude/scripts/write-state.mjs" "$S/agent-state.json" >/dev/null
13
+ export MULTI_AGENT_TASK_ID="$TASK_ID"
14
+ bash "$HOME/.claude/scripts/phase-tracker.sh" init "$TASK_ID"
15
+ for p in "0:Input" "1:Diff" "2:Reviewers" "3:Triage" "4:Report"; do
16
+ bash "$HOME/.claude/scripts/phase-tracker.sh" add "${p%%:*}" "${p#*:}"
17
+ done
18
+ bash "$HOME/.claude/scripts/phase-tracker.sh" update 0 in_progress ;;
19
+ esac
20
+ ```
21
+
22
+ Step 1 (1, 1a) is tracker step 0, step 2 (2-2c) step 1, steps 3-4b step 2,
23
+ step 5 step 3 and steps 6-7 step 4: `update <N> in_progress` on entry and `completed` on exit. Each later
24
+ call runs in a new shell where `$TASK_ID` is no longer set, so it carries the
25
+ review's id written out literally (the `review-...` value the registration
26
+ used) and refuses the global pointer, which another run's `init` can move:
27
+
28
+ ```bash
29
+ MULTI_AGENT_TASK_ID=review-1 MULTI_AGENT_STRICT_TASK_ID=1 \
30
+ bash "$HOME/.claude/scripts/phase-tracker.sh" update 2 completed
31
+ ```
32
+
33
+ After step 7 a standalone review closes its record with the same `write-state.mjs` call and
34
+ `"status":"complete","currentPhase":4`.
35
+
36
+ Reviewer token counts (`phase-tracker.sh tokens <N> ...`) go under step 2
37
+ (Reviewers); inside a pipeline run they go under the run's Review phase.
@@ -93,7 +93,7 @@ Phases by mode:
93
93
  | `/multi-agent` | 0,1,2,3,4,5 |
94
94
  | answered local at Step 5b | 0,1,2,3,4,5 (the user test needs a worktree checkout and local has none, so that STEP inside Review is skipped - the phase is not) |
95
95
  | `/multi-agent:autopilot` | 0,1,2,3,4,5 (autopilot drops the interactive user test inside Review) |
96
- | `/multi-agent:analysis` | 0,1,3,4,5 (no code is written, so Dev is not in the set) |
96
+ | `/multi-agent:analysis` | 0,1,2,3,4,5 under its own names (`schemas/phases.json` `modes.analysis.names`: Intake, Evidence, Synthesis, Review, Publish, Report) |
97
97
 
98
98
  Every entry registers its whole set in one batch: the set is a property of the command, known before the tracker boots.
99
99
 
@@ -404,7 +404,7 @@ call for call, so a reader that is not the host (`runs-index.mjs`, and every
404
404
  client behind the contract server) sees the same list under the same phase:
405
405
 
406
406
  ```bash
407
- bash $HOME/.claude/scripts/phase-tracker.sh task <N> <task_id> pending|in_progress|completed \
407
+ bash $HOME/.claude/scripts/phase-tracker.sh task <N> <task_id> pending|in_progress|completed|deleted \
408
408
  [--subject "<subject>"] [--active-form "<activeForm>"]
409
409
  ```
410
410
 
@@ -416,10 +416,15 @@ bash $HOME/.claude/scripts/phase-tracker.sh task <N> <task_id> pending|in_progre
416
416
 
417
417
  `<N>` is the phase the task belongs to: the phase in progress when it was
418
418
  created. A new task needs `--subject`; an update may pass the status alone and
419
- keeps the subject and active form it had. Phase tiles themselves are not
419
+ keeps the subject and active form it had. `deleted` takes the task off the list, as a native `TaskUpdate` with status `deleted` does. Phase tiles themselves are not
420
420
  mirrored: the phase list already is the state file. The call is quiet (it
421
421
  renders nothing), so it never replaces the host's own update; it records it.
422
422
 
423
+ On Claude Code the `task-mirror.sh` PostToolUse hook makes this call itself
424
+ for every `TaskCreate` and `TaskUpdate` of the session bound to the run, under
425
+ the phase in progress, and leaves phase tiles out. A manual `task` call on top
426
+ of it is harmless: the second write sets the same values.
427
+
423
428
  ## Resume behaviour
424
429
 
425
430
  When `/multi-agent:resume <task_id>` is called:
@@ -4,7 +4,10 @@ The office page (`index.html`, built from `pipeline/office-src` by
4
4
  `pipeline/scripts/office-build.mjs`) draws sprites from pixel-agents
5
5
  (https://github.com/pixel-agents-hq/pixel-agents, commit b1bafd0): the six
6
6
  character sheets, the floor and wall tiles and the furniture, kept unchanged
7
- in `assets/`. pixel-agents'
7
+ in `assets/`. The company furniture (kitchen, restroom, server room, archive,
8
+ reception, lab and mail room pieces) is this project's own drawing: its
9
+ sources are the pixel grids in `pipeline/office-art`, which the build renders
10
+ into `assets/` with the pixel-agents palette. pixel-agents'
8
11
  characters are based on JIK-A-4's MetroCity free top-down character pack
9
12
  (https://jik-a-4.itch.io/metrocity-free-topdown-character-pack), released
10
13
  under CC0 1.0.