amicus 4.4.0 → 4.5.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 (109) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +162 -0
  3. package/README.md +17 -2
  4. package/bin/amicus.js +10 -0
  5. package/docs/DISTRIBUTION.md +234 -0
  6. package/docs/ROADMAP.md +226 -0
  7. package/docs/SHIMS.md +62 -0
  8. package/docs/architecture.md +104 -0
  9. package/docs/configuration.md +395 -0
  10. package/docs/council.md +970 -0
  11. package/docs/doc-system.md +92 -0
  12. package/docs/electron-testing.md +471 -0
  13. package/docs/jsdoc-setup.md +75 -0
  14. package/docs/opencode-integration.md +114 -0
  15. package/docs/publishing.md +60 -0
  16. package/docs/schemas.md +56 -0
  17. package/docs/testing.md +589 -0
  18. package/docs/troubleshooting.md +298 -0
  19. package/docs/usage.md +849 -0
  20. package/electron/fold.js +1 -1
  21. package/electron/main.js +4 -1
  22. package/electron/setup-ui-aliases.js +6 -6
  23. package/electron/workspace-ui/live-model.js +12 -1
  24. package/electron/workspace-ui/md-lite.js +52 -8
  25. package/electron/workspace-ui/workspace-app.js +39 -17
  26. package/electron/workspace-ui/workspace-matrix.js +46 -9
  27. package/electron/workspace-ui/workspace-panels.js +88 -19
  28. package/electron/workspace-ui/workspace-render.js +17 -1
  29. package/electron/workspace-ui/workspace-verbs.js +48 -2
  30. package/package.json +8 -3
  31. package/schemas/council-run-live.schema.json +1 -1
  32. package/schemas/council-run.schema.json +34 -0
  33. package/schemas/error.schema.json +1 -1
  34. package/schemas/event.schema.json +1 -1
  35. package/schemas/pack.schema.json +30 -0
  36. package/schemas/progress.schema.json +13 -1
  37. package/schemas/run-live.schema.json +1 -1
  38. package/schemas/run.schema.json +2 -1
  39. package/schemas/spend.schema.json +52 -4
  40. package/schemas/wave-live.schema.json +1 -1
  41. package/schemas/wave.schema.json +2 -1
  42. package/skills/second-opinion/SKILL.md +5 -0
  43. package/src/cli-handlers-council-run.js +51 -8
  44. package/src/cli-handlers-pack.js +238 -0
  45. package/src/cli-handlers-run.js +36 -8
  46. package/src/cli-handlers-spend.js +20 -2
  47. package/src/cli-handlers-template.js +53 -0
  48. package/src/cli-handlers-watch.js +11 -0
  49. package/src/cli.js +68 -5
  50. package/src/council/briefings-debate.js +27 -7
  51. package/src/council/briefings-stage2.js +155 -25
  52. package/src/council/briefings.js +24 -1
  53. package/src/council/findings.js +199 -9
  54. package/src/council/parse-stage2.js +10 -2
  55. package/src/council/presets-cli.js +23 -11
  56. package/src/council/report.js +19 -8
  57. package/src/council/run-assemble.js +42 -1
  58. package/src/council/run-budget.js +64 -11
  59. package/src/council/run-chair.js +4 -1
  60. package/src/council/run-debate.js +4 -2
  61. package/src/council/run-finalize.js +102 -0
  62. package/src/council/run-launch.js +29 -1
  63. package/src/council/run-server.js +248 -0
  64. package/src/council/run-stage2.js +118 -0
  65. package/src/council/run-stages.js +134 -110
  66. package/src/council/run-state.js +40 -1
  67. package/src/council/run.js +45 -47
  68. package/src/council/tally.js +10 -0
  69. package/src/headless.js +180 -7
  70. package/src/mcp-council-run.js +108 -4
  71. package/src/mcp-server.js +203 -7
  72. package/src/mcp-tools.js +15 -5
  73. package/src/observe/council-legs.js +60 -3
  74. package/src/observe/live-doc.js +18 -1
  75. package/src/observe/watch-render.js +4 -1
  76. package/src/pack/pack-cli.js +38 -0
  77. package/src/pack/pack-forward.js +96 -0
  78. package/src/pack/pack-resolve.js +297 -0
  79. package/src/pack/pack-store.js +130 -0
  80. package/src/pack/pack-validate.js +113 -0
  81. package/src/sidecar/child-sessions.js +1 -2
  82. package/src/sidecar/fanout-leg-fallback.js +69 -21
  83. package/src/sidecar/fanout-leg.js +6 -0
  84. package/src/sidecar/fanout-signals.js +61 -0
  85. package/src/sidecar/fanout-wave-io.js +75 -0
  86. package/src/sidecar/fanout.js +82 -74
  87. package/src/sidecar/progress-fields.js +26 -4
  88. package/src/sidecar/progress.js +42 -1
  89. package/src/sidecar/session-utils.js +23 -14
  90. package/src/sidecar/start.js +5 -4
  91. package/src/sidecar/workspace-auto-open.js +69 -0
  92. package/src/sidecar/workspace-window.js +46 -1
  93. package/src/spend-query.js +17 -5
  94. package/src/template/apply.js +88 -0
  95. package/src/template/render.js +86 -0
  96. package/src/template/store.js +106 -0
  97. package/src/utils/config.js +65 -25
  98. package/src/utils/error-doc.js +5 -0
  99. package/src/utils/lifecycle.js +37 -1
  100. package/src/utils/path-fence.js +39 -1
  101. package/src/utils/pricing.js +26 -10
  102. package/src/utils/result-schema-rebuild.js +1 -0
  103. package/src/utils/result-schema.js +8 -2
  104. package/src/utils/server-setup.js +79 -1
  105. package/src/utils/spend-ledger.js +24 -3
  106. package/src/workspace/artifact-guard.js +66 -7
  107. package/src/workspace/fold-format.js +33 -4
  108. package/src/workspace/live-normalize.js +28 -15
  109. package/src/workspace/run-detail.js +13 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "mcpName": "io.github.BourbonDog/amicus",
5
5
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
6
6
  "keywords": [
@@ -43,6 +43,7 @@
43
43
  "commands/",
44
44
  ".claude-plugin/",
45
45
  "CHANGELOG.md",
46
+ "docs/*.md",
46
47
  "scripts/postinstall.js",
47
48
  "scripts/setup-hooks.js"
48
49
  ],
@@ -50,11 +51,11 @@
50
51
  "start": "node --experimental-top-level-await --experimental-vm-modules bin/amicus.js",
51
52
  "test": "jest",
52
53
  "test:integration": "node scripts/run-integration-keyless.js",
53
- "test:integration:live": "jest --testPathIgnorePatterns='worktrees' --testMatch='**/tests/**/*.integration.test.js'",
54
+ "test:integration:live": "jest --runInBand --testPathIgnorePatterns='worktrees' --testMatch='**/tests/**/*.integration.test.js'",
54
55
  "test:all": "jest --testPathIgnorePatterns='/node_modules/' --testPathIgnorePatterns='worktrees' && node scripts/mark-test-passed.js",
55
56
  "test:e2e:mcp": "jest tests/mcp-repomix-e2e.integration.test.js --testTimeout=180000 --forceExit",
56
57
  "posttest": "node scripts/mark-test-passed.js",
57
- "lint": "eslint src/",
58
+ "lint": "eslint src/ electron/",
58
59
  "postinstall": "node scripts/postinstall.js",
59
60
  "test:thinking": "node scripts/benchmark-thinking.js",
60
61
  "test:thinking:quick": "MODELS=gemini node scripts/benchmark-thinking.js",
@@ -88,6 +89,7 @@
88
89
  "ajv": "^8.20.0",
89
90
  "chrome-remote-interface": "^0.33.3",
90
91
  "eslint": "^8.0.0",
92
+ "espree": "^9.6.1",
91
93
  "jest": "^29.0.0",
92
94
  "lint-staged": "^16.3.2",
93
95
  "puppeteer": "^24.36.0",
@@ -100,6 +102,9 @@
100
102
  "lint-staged": {
101
103
  "src/**/*.js": [
102
104
  "eslint --fix"
105
+ ],
106
+ "electron/**/*.js": [
107
+ "eslint --fix"
103
108
  ]
104
109
  }
105
110
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/BourbonDog/amicus/schemas/council-run-live.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/council-run-live.schema.json",
4
4
  "title": "Amicus composed live council-run doc (amicus_status/buildCouncilStatusPayload, view:'live')",
5
5
  "type": "object",
6
6
  "required": ["taskId", "type", "runId", "runDir", "status", "currentStage", "stages", "legsTotal", "legsComplete", "elapsed", "exitCode", "version"],
@@ -30,8 +30,22 @@
30
30
  "chair": { "type": "string" },
31
31
  "critic": { "type": ["string", "null"] },
32
32
  "lenses": { "type": ["array", "null"], "items": { "type": "string" } },
33
+ "droppedMembers": {
34
+ "description": "Council preset members dropped during resolution before this run started (v4.5 Wave 2): an alias that no longer resolves, or a resolved id absent from a non-empty cached model catalog (a catalog-unknown/offline member is never dropped this way). Present only when at least one member was actually dropped; absent — never an empty array — otherwise. `bench` above already reflects the survivors only; this is purely an additional observability signal so a scripted/MCP caller can detect a shrunken bench without diffing `bench` against the preset's nominal member list.",
35
+ "type": "array",
36
+ "items": {
37
+ "type": "object",
38
+ "required": ["member", "reason"],
39
+ "properties": {
40
+ "member": { "type": "string" },
41
+ "reason": { "type": "string" }
42
+ }
43
+ }
44
+ },
33
45
  "labelMap": { "type": ["object", "null"], "additionalProperties": { "type": "string" } },
34
46
  "options": { "type": "object" },
47
+ "pack": { "type": "object" },
48
+ "template": { "type": "object" },
35
49
  "usage": { "type": "object" },
36
50
  "exitCode": { "type": ["number", "null"] },
37
51
  "budgetRefusals": {
@@ -48,6 +62,26 @@
48
62
  }
49
63
  }
50
64
  },
65
+ "sharedServer": {
66
+ "description": "The run acquired its single shared OpenCode server and threaded it through every wave (v4.4.1 Task 0.5). Present only on a successful acquisition; mutually exclusive with sharedServerUnavailable. `goPid` is the shared server's process id — no wave writes a goPid into its own metadata.json while riding an injected server, so this field is the POSITIVE, direct evidence that one server served the run, replacing the goPid-absence inference.",
67
+ "type": "object",
68
+ "required": ["acquired"],
69
+ "properties": {
70
+ "acquired": { "const": true },
71
+ "at": { "type": "string" },
72
+ "goPid": { "type": ["number", "null"] },
73
+ "models": { "type": "number" }
74
+ }
75
+ },
76
+ "sharedServerUnavailable": {
77
+ "description": "The run could not start its single shared OpenCode server and fell back to one server per wave (v4.4.1 Task 0.5). Present only when the acquisition failed. The run CONTINUES — this is never fatal — but the per-wave configuration is the one that races on OpenCode's SQLite, so results may be degraded.",
78
+ "type": "object",
79
+ "required": ["error"],
80
+ "properties": {
81
+ "error": { "type": "string" },
82
+ "at": { "type": "string" }
83
+ }
84
+ },
51
85
  "debate": {
52
86
  "type": "object",
53
87
  "properties": {
@@ -13,7 +13,7 @@
13
13
  "type": "object",
14
14
  "required": ["code", "message"],
15
15
  "properties": {
16
- "code": { "enum": ["BAD_ARGS", "MISSING_PROMPT", "BAD_MODEL", "MISSING_KEY", "BAD_SESSION", "BUDGET_EXCEEDED", "INTERNAL", "COUNCIL_QUORUM", "COST_EXCEEDED", "COUNCIL_CLAUDE_REVIEW_INVALID"] },
16
+ "code": { "enum": ["BAD_ARGS", "MISSING_PROMPT", "BAD_MODEL", "MISSING_KEY", "BAD_SESSION", "BUDGET_EXCEEDED", "INTERNAL", "COUNCIL_QUORUM", "COST_EXCEEDED", "COUNCIL_CLAUDE_REVIEW_INVALID", "TEMPLATE_NOT_FOUND", "TEMPLATE_RENDER", "PACK_NOT_FOUND", "PACK_INVALID", "PACK_KIND_MISMATCH"] },
17
17
  "message": { "type": "string" },
18
18
  "hint": { "type": ["string", "null"] },
19
19
  "command": { "type": ["string", "null"] }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/BourbonDog/amicus/schemas/event.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/event.schema.json",
4
4
  "title": "Amicus observability event (events.jsonl line)",
5
5
  "type": "object",
6
6
  "required": ["schemaVersion", "type", "event", "ts", "id"],
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/pack.schema.json",
4
+ "title": "Amicus policy pack",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "type", "name", "version", "kind"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "type": { "const": "pack" },
10
+ "name": { "type": "string", "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$" },
11
+ "version": { "type": "string" },
12
+ "kind": { "enum": ["council", "fanout", "solo"] },
13
+ "description": { "type": "string" },
14
+ "bench": {
15
+ "oneOf": [
16
+ { "type": "string" },
17
+ { "type": "array", "items": { "type": "string" }, "minItems": 2 }
18
+ ]
19
+ },
20
+ "model": { "type": "string" },
21
+ "chair": { "type": ["string", "null"] },
22
+ "critic": { "type": ["string", "null"] },
23
+ "lenses": { "type": ["array", "null"], "items": { "type": "string" } },
24
+ "options": { "type": "object" },
25
+ "briefing": {
26
+ "type": "object",
27
+ "properties": { "template": { "type": ["string", "null"] } }
28
+ }
29
+ }
30
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/BourbonDog/amicus/schemas/progress.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/progress.schema.json",
4
4
  "title": "Amicus leg/solo progress snapshot (progress.json)",
5
5
  "type": "object",
6
6
  "required": ["schemaVersion", "type", "stage", "updatedAt"],
@@ -12,6 +12,18 @@
12
12
  "updatedAt": { "type": "string", "format": "date-time" },
13
13
  "messagesReceived": { "type": "number" },
14
14
  "latestTool": { "type": "string" },
15
+ "toolSettleTimedOut": {
16
+ "description": "v4.4 B4. The leg was completed while at least one tool call was still non-terminal, after AMICUS_TOOL_SETTLE_GRACE_MS elapsed. Its partial output was kept and it was NOT failed, but its reported cost is a floor. Present only when true.",
17
+ "const": true
18
+ },
19
+ "unsettledToolCalls": {
20
+ "description": "How many tool calls were still live when the settle grace was exceeded. A count here (progress.json is a compact snapshot); the full list rides the runHeadless result. Present only alongside toolSettleTimedOut.",
21
+ "type": "number"
22
+ },
23
+ "toolSettleAborted": {
24
+ "description": "v4.4.1 LC-2. Whether the leg's OpenCode session was successfully aborted at the settle ceiling so it would stop billing. `false` means the abort was attempted and failed — the session may still be billing. Present only alongside toolSettleTimedOut.",
25
+ "type": "boolean"
26
+ },
15
27
  "usage": {
16
28
  "type": "object",
17
29
  "properties": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/BourbonDog/amicus/schemas/run-live.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/run-live.schema.json",
4
4
  "title": "Amicus composed live single-session doc (amicus_status, view:'live')",
5
5
  "description": "amicus_status's single-session response always stamps type:'run' (stampEnvelope); documented here as a const rather than the brief's 'may be absent' guess to match real output.",
6
6
  "type": "object",
@@ -21,6 +21,7 @@
21
21
  "durationMs": { "type": ["number", "null"] },
22
22
  "sessionDir": { "type": ["string", "null"] },
23
23
  "opencodeSessionId": { "type": ["string", "null"] },
24
- "usage": { "type": ["object", "null"] }
24
+ "usage": { "type": ["object", "null"] },
25
+ "pack": { "type": "object" }
25
26
  }
26
27
  }
@@ -9,8 +9,28 @@
9
9
  "schemaVersion": { "const": 2 },
10
10
  "type": { "const": "spend" },
11
11
  "windowDays": { "type": ["number", "null"] },
12
- "total": { "type": "object" },
13
- "byModel": { "type": "array", "items": { "type": "object" } },
12
+ "total": {
13
+ "type": "object",
14
+ "properties": {
15
+ "amount": { "type": "number" },
16
+ "runs": { "type": "number" },
17
+ "unpricedRows": { "$ref": "#/$defs/unpricedRows" },
18
+ "unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" }
19
+ }
20
+ },
21
+ "byModel": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "properties": {
26
+ "model": { "type": "string" },
27
+ "amount": { "type": "number" },
28
+ "runs": { "type": "number" },
29
+ "unpricedRows": { "$ref": "#/$defs/unpricedRows" },
30
+ "unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" }
31
+ }
32
+ }
33
+ },
14
34
  "credit": { "type": ["object", "null"] },
15
35
  "filters": { "type": "object" },
16
36
  "groupBy": { "enum": ["model", "wave", "council", "project", "op", "day"] },
@@ -22,7 +42,9 @@
22
42
  "properties": {
23
43
  "key": { "type": "string" },
24
44
  "amount": { "type": "number" },
25
- "runs": { "type": "number" }
45
+ "runs": { "type": "number" },
46
+ "unpricedRows": { "$ref": "#/$defs/unpricedRows" },
47
+ "unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" }
26
48
  }
27
49
  }
28
50
  },
@@ -32,10 +54,36 @@
32
54
  "properties": {
33
55
  "amount": { "type": "number" },
34
56
  "runs": { "type": "number" },
57
+ "unpricedRows": { "$ref": "#/$defs/unpricedRows" },
58
+ "unattributedSubtreeRows": { "$ref": "#/$defs/unattributedSubtreeRows" },
35
59
  "byStatus": { "type": "object" }
36
60
  }
37
61
  },
38
- "rows": { "type": "array" },
62
+ "rows": {
63
+ "type": "array",
64
+ "items": {
65
+ "type": "object",
66
+ "description": "A raw spend-ledger.jsonl row (internal v1 shape, echoed verbatim when --rows is passed). Always an object: readSpendRows drops any ledger line that does not parse as one, scalars and arrays included. Only the fields this document makes claims about are pinned beyond that.",
67
+ "properties": {
68
+ "subtreeUnknown": {
69
+ "type": "boolean",
70
+ "description": "v4.4.1. Present (and always true) only when this leg's own cost resolved but it spawned a child session whose spend could not be determined — the row is PRICED yet its cost is a floor. OMITTED entirely otherwise, so an ordinary row is byte-identical to a pre-4.4.1 one."
71
+ }
72
+ }
73
+ }
74
+ },
39
75
  "rowsTruncated": { "type": "boolean" }
76
+ },
77
+ "$defs": {
78
+ "unpricedRows": {
79
+ "type": "integer",
80
+ "minimum": 0,
81
+ "description": "Rows that contributed NOTHING to `amount` (no numeric cost). The bucket's figure omits them entirely: real spend is at least this much."
82
+ },
83
+ "unattributedSubtreeRows": {
84
+ "type": "integer",
85
+ "minimum": 0,
86
+ "description": "v4.4.1 CA-2. Rows that ARE priced and DO contribute to `amount`, but whose leg spawned a subagent whose child-session spend could not be determined — the second, independent reason the figure is a floor. A row can be counted in both this and `unpricedRows`; the two are never substitutes."
87
+ }
40
88
  }
41
89
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://github.com/BourbonDog/amicus/schemas/wave-live.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/BourbonDog/amicus/main/schemas/wave-live.schema.json",
4
4
  "title": "Amicus composed live wave doc (amicus_status, view:'live')",
5
5
  "type": "object",
6
6
  "required": ["taskId", "type", "status", "legs"],
@@ -28,6 +28,7 @@
28
28
  "completedAt": { "type": ["string", "null"] },
29
29
  "durationMs": { "type": ["number", "null"] },
30
30
  "usage": { "type": "object" },
31
- "notices": { "type": "array", "items": { "type": "string" } }
31
+ "notices": { "type": "array", "items": { "type": "string" } },
32
+ "pack": { "type": "object" }
32
33
  }
33
34
  }
@@ -211,6 +211,11 @@ mangled alias (instant arg-parse failure). For a free council, swap `--models "<
211
211
  `--council free`. Run it in the background (`run_in_background: true`); you are notified on
212
212
  completion — do not poll.
213
213
 
214
+ **Saved run configs (v4.5).** Every flag above this line can also come from a saved
215
+ [policy pack](../../docs/usage.md#policy-packs) — `--pack <name>` loads its bench/chair/critic/
216
+ lenses/options/template as defaults (an explicit flag still overrides it), the same on the
217
+ `amicus_council_run`/`amicus_fanout`/`amicus_start` MCP tools' `pack` param.
218
+
214
219
  **Budget gate — one flag for the whole run.** By default the gate refuses any leg whose price
215
220
  exceeds the per-$/Mtok threshold (the o3/o3-pro guard). To run an intentionally expensive model the
216
221
  user asked for by name, pass `--no-cost-gate`; to raise only the total ceiling, pass
@@ -71,9 +71,11 @@ function resolveBench(args, useJson) {
71
71
  if (expanded.dropped && expanded.dropped.length && !useJson) {
72
72
  process.stderr.write(`Notice: dropped unavailable council member(s): ${expanded.dropped.join(', ')}\n`);
73
73
  }
74
- return { bench: expanded.models, presetName };
74
+ // v4.5 Wave 2: threaded into runCouncil's options — the ONLY prior signal
75
+ // was the stderr-only Notice above, which --json mode never even prints.
76
+ return { bench: expanded.models, presetName, droppedMembers: expanded.droppedMembers || [] };
75
77
  }
76
- return { bench: parseList(args.models), presetName: null };
78
+ return { bench: parseList(args.models), presetName: null, droppedMembers: [] };
77
79
  }
78
80
 
79
81
  function renderRunHuman(run) {
@@ -115,6 +117,27 @@ function renderRunHuman(run) {
115
117
  async function handleCouncilRun(args) {
116
118
  const useJson = !!args.json;
117
119
 
120
+ // v4.5 Task 12 (B7/F5): resolve --pack FIRST, above the Task-5 template
121
+ // block, so a pack-filled args.template renders through that single
122
+ // existing application point exactly like a typed --template.
123
+ let packRecord = null;
124
+ const explicitKeys = args.__explicit || new Set();
125
+ if (args.pack !== undefined) {
126
+ const { applyPackToArgs } = require('./pack/pack-resolve');
127
+ const pr = applyPackToArgs({
128
+ packRef: args.pack, expectedKind: 'council', args,
129
+ explicit: explicitKeys, useJson,
130
+ });
131
+ if (pr.error) { return failJson(useJson, pr.error); }
132
+ for (const n of pr.notices) { process.stderr.write(n + '\n'); }
133
+ packRecord = pr.packRecord;
134
+ }
135
+ // 2026-07-28 ruling (Task-11 review): attribute a pre-flight failure to the
136
+ // pack that supplied the failing value — ONLY when the pack filled it (an
137
+ // explicit flag always wins and is never "blamed" on the pack).
138
+ const packSuffix = (key) => (packRecord && args[key] !== undefined && !explicitKeys.has(key))
139
+ ? ` (set by pack '${packRecord.name}')` : '';
140
+
118
141
  // --prompt-file required; inline --prompt rejected (councils always have
119
142
  // real briefings — same rationale as MCP fanout's briefing-via-file).
120
143
  if (args.prompt !== undefined) {
@@ -123,9 +146,26 @@ async function handleCouncilRun(args) {
123
146
  hint: 'write the briefing to a file and pass --prompt-file <path>' });
124
147
  }
125
148
  const { resolvePromptSource } = require('./utils/prompt-source');
126
- const promptRes = resolvePromptSource(args);
127
- if (promptRes.error) {
128
- return failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error });
149
+ // F9 (v4.5): with --template and no {{prompt}} slot, --prompt-file may be
150
+ // absent (mirrors handleFanout's guard); byte-identical without --template.
151
+ let promptRes;
152
+ if (args.prompt !== undefined || args['prompt-file'] !== undefined || args.template === undefined) {
153
+ promptRes = resolvePromptSource(args);
154
+ if (promptRes.error) { return failJson(useJson, { code: ERROR_CODES.MISSING_PROMPT, message: promptRes.error }); }
155
+ } else {
156
+ promptRes = { prompt: undefined, promptMeta: null };
157
+ }
158
+ let templateMeta = null;
159
+ if (args.template !== undefined) {
160
+ const { applyTemplate } = require('./template/apply');
161
+ const t = applyTemplate({ templateRef: args.template, prompt: promptRes.prompt,
162
+ artifactFile: args.artifact, varList: args.var, project: args.cwd || process.cwd() });
163
+ if (t.error) { return failJson(useJson, t.error); }
164
+ for (const n of t.notices) { process.stderr.write(n + '\n'); }
165
+ promptRes = { prompt: t.prompt, promptMeta: t.promptMeta };
166
+ templateMeta = t.promptMeta.template;
167
+ } else if (args.artifact !== undefined || args.var !== undefined) {
168
+ return failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: 'Error: --artifact/--var require --template (expansion happens only in template files)' });
129
169
  }
130
170
 
131
171
  const benchRes = resolveBench(args, useJson);
@@ -153,19 +193,19 @@ async function handleCouncilRun(args) {
153
193
  ? args.chair.trim() : CHAIR_DEFAULT;
154
194
  if (bench.includes(chair)) {
155
195
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
156
- message: `Error: chair '${chair}' is a bench seat — the chair must not review`,
196
+ message: `Error: chair '${chair}' is a bench seat — the chair must not review${packSuffix('chair')}`,
157
197
  hint: `pick a chair outside --models (default: ${CHAIR_DEFAULT}), or remove '${chair}' from the bench` });
158
198
  }
159
199
  const critic = (typeof args.critic === 'string' && args.critic.trim()) ? args.critic.trim() : null;
160
200
  if (critic && !bench.includes(critic)) {
161
201
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
162
- message: `Error: critic '${critic}' must be one of the bench seats`,
202
+ message: `Error: critic '${critic}' must be one of the bench seats${packSuffix('critic')}`,
163
203
  hint: `--critic swaps one seat's brief; pass one of: ${bench.join(', ')}` });
164
204
  }
165
205
  const lenses = (typeof args.lenses === 'string' && args.lenses.trim()) ? parseList(args.lenses) : null;
166
206
  if (critic && lenses) {
167
207
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
168
- message: 'Error: --critic and --lenses are mutually exclusive in v4.0' });
208
+ message: `Error: --critic and --lenses are mutually exclusive in v4.0${packSuffix('critic') || packSuffix('lenses')}` });
169
209
  }
170
210
  if (lenses && lenses.length !== bench.length) {
171
211
  return failJson(useJson, { code: ERROR_CODES.BAD_ARGS,
@@ -211,6 +251,9 @@ async function handleCouncilRun(args) {
211
251
  noValidateModel: !!args['no-validate-model'],
212
252
  date: new Date().toISOString().slice(0, 10),
213
253
  councilName,
254
+ template: templateMeta, // F9 (v4.5): null when no --template; additive on the run.json seed (run-state.js).
255
+ pack: packRecord, // v4.5 Task 12 (B7/F5): null when no --pack; additive on the run.json seed (run-state.js).
256
+ droppedMembers: benchRes.droppedMembers, // v4.5 Wave 2: [] when nothing dropped; additive on the run.json seed (run-state.js).
214
257
  // v4.1 §4.5b/§4.5d. `--claude-review` is resolved here but VALIDATED by the
215
258
  // engine's preflightClaudeReview (run-assemble.js): the reserved-seat and
216
259
  // 'claude may not chair' guards live there on purpose so MCP, the GitHub