amicus 4.3.0 → 4.4.1

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 (94) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +64 -0
  3. package/README.md +6 -3
  4. package/docs/DISTRIBUTION.md +234 -0
  5. package/docs/ROADMAP.md +200 -0
  6. package/docs/SHIMS.md +62 -0
  7. package/docs/architecture.md +104 -0
  8. package/docs/configuration.md +371 -0
  9. package/docs/council.md +911 -0
  10. package/docs/doc-system.md +92 -0
  11. package/docs/electron-testing.md +471 -0
  12. package/docs/jsdoc-setup.md +75 -0
  13. package/docs/opencode-integration.md +114 -0
  14. package/docs/publishing.md +60 -0
  15. package/docs/schemas.md +55 -0
  16. package/docs/testing.md +589 -0
  17. package/docs/troubleshooting.md +298 -0
  18. package/docs/usage.md +699 -0
  19. package/electron/fold.js +1 -1
  20. package/electron/ipc-workspace.js +283 -0
  21. package/electron/main.js +31 -1
  22. package/electron/preload-workspace.js +40 -0
  23. package/electron/setup-ui-aliases.js +6 -6
  24. package/electron/workspace-shell.js +85 -0
  25. package/electron/workspace-ui/index.html +111 -0
  26. package/electron/workspace-ui/live-model.js +112 -0
  27. package/electron/workspace-ui/md-lite.js +163 -0
  28. package/electron/workspace-ui/workspace-app.js +240 -0
  29. package/electron/workspace-ui/workspace-matrix.js +249 -0
  30. package/electron/workspace-ui/workspace-panels.js +237 -0
  31. package/electron/workspace-ui/workspace-render.js +277 -0
  32. package/electron/workspace-ui/workspace-verbs.js +293 -0
  33. package/electron/workspace-ui/workspace.css +172 -0
  34. package/package.json +8 -3
  35. package/schemas/council-run-live.schema.json +25 -1
  36. package/schemas/council-run.schema.json +34 -0
  37. package/schemas/progress.schema.json +26 -1
  38. package/schemas/spend.schema.json +52 -4
  39. package/skills/second-opinion/MODEL-NOTES.md +53 -5
  40. package/src/cli-handlers-council-run.js +25 -3
  41. package/src/cli-handlers-spend.js +50 -5
  42. package/src/cli-handlers-watch.js +48 -10
  43. package/src/cli.js +4 -2
  44. package/src/council/briefings-debate.js +27 -7
  45. package/src/council/briefings-stage2.js +155 -25
  46. package/src/council/briefings.js +59 -3
  47. package/src/council/findings.js +236 -9
  48. package/src/council/parse-stage2.js +10 -2
  49. package/src/council/report.js +19 -8
  50. package/src/council/run-assemble.js +42 -1
  51. package/src/council/run-budget.js +277 -0
  52. package/src/council/run-chair.js +4 -1
  53. package/src/council/run-debate.js +4 -2
  54. package/src/council/run-finalize.js +102 -0
  55. package/src/council/run-launch.js +73 -7
  56. package/src/council/run-server.js +248 -0
  57. package/src/council/run-stage2.js +118 -0
  58. package/src/council/run-stages.js +148 -113
  59. package/src/council/run-state.js +23 -1
  60. package/src/council/run.js +52 -53
  61. package/src/council/tally.js +10 -0
  62. package/src/headless.js +519 -17
  63. package/src/mcp-council-awareness.js +53 -3
  64. package/src/observe/council-legs.js +240 -0
  65. package/src/observe/live-doc.js +39 -4
  66. package/src/observe/watch-render.js +23 -1
  67. package/src/opencode-client.js +15 -3
  68. package/src/sidecar/child-sessions.js +197 -0
  69. package/src/sidecar/conversation-mirror.js +111 -37
  70. package/src/sidecar/fanout-budget.js +71 -0
  71. package/src/sidecar/fanout-leg-fallback.js +69 -21
  72. package/src/sidecar/fanout-leg.js +29 -1
  73. package/src/sidecar/fanout-signals.js +61 -0
  74. package/src/sidecar/fanout-wave-io.js +75 -0
  75. package/src/sidecar/fanout.js +65 -81
  76. package/src/sidecar/progress-fields.js +26 -4
  77. package/src/sidecar/progress.js +8 -1
  78. package/src/sidecar/session-utils.js +23 -14
  79. package/src/sidecar/tool-part.js +196 -0
  80. package/src/sidecar/workspace-window.js +62 -0
  81. package/src/spend-query.js +33 -6
  82. package/src/utils/env-num.js +42 -0
  83. package/src/utils/lifecycle.js +37 -1
  84. package/src/utils/path-fence.js +120 -0
  85. package/src/utils/pricing.js +114 -9
  86. package/src/utils/server-setup.js +79 -1
  87. package/src/utils/spend-ledger.js +24 -3
  88. package/src/workspace/artifact-guard.js +208 -0
  89. package/src/workspace/blind-mode.js +32 -0
  90. package/src/workspace/fold-format.js +124 -0
  91. package/src/workspace/live-normalize.js +169 -0
  92. package/src/workspace/matrix-model.js +94 -0
  93. package/src/workspace/run-detail.js +229 -0
  94. package/src/workspace/run-scan.js +148 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Council Workspace — run discovery (v4.4 spec §4.3 / §5.1).
3
+ *
4
+ * Walks the project sessions dir for v4.0 council pointer files
5
+ * (`council-<runId>.json` = {runId, runDir}) and builds run-list rows from a
6
+ * shallow, defensive read of run.json (+ verdict.json for the overallVerdict
7
+ * chip). Strictly read-only (§6.2). Unreadable pointers/runs come back as
8
+ * {runId, error} rows — this module never throws on bad input.
9
+ */
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { formatCost } = require('../utils/pricing');
15
+ // ⚠️ DE-ROT (F25): v4.0 module (not v4.3) — safe for Phase 1. Single source of truth
16
+ // for reading/validating council pointer files; do not re-parse them here.
17
+ const runState = require('../council/run-state');
18
+ // Same constant run-state.js itself imports (src/council/run-state.js:20), so a
19
+ // future rename can't silently desync this module's walk from its pointer reads.
20
+ const { SESSIONS_DIR } = require('../session-manager');
21
+ // ⚠️ THIRD COUNCIL-REVIEW PASS: readPointer (below) only validates {runId, runDir}
22
+ // for truthiness — a tampered/stale pointer can point runDir anywhere on disk. This
23
+ // is the shared realpath-containment primitive (also used by artifact-guard.js's
24
+ // readRunArtifact and run-detail.js's getRunDetail); it lives in its own leaf module
25
+ // specifically so this file can require it without creating a cycle — artifact-guard.js
26
+ // already requires THIS file for readPointer.
27
+ const { isRealpathContained } = require('../utils/path-fence');
28
+
29
+ // ⚠️ DE-ROT (F25): widened from /^council-([0-9a-f]{8})\.json$/ to match the shipped
30
+ // walker's pattern (src/council/run-state.js:148) so ids that are not 8 hex still list.
31
+ const POINTER_RE = /^council-([a-zA-Z0-9_-]{1,64})\.json$/;
32
+
33
+ // Same character class as POINTER_RE's capture group / run-state.js:148's enumeration
34
+ // pattern. readPointer() below is a DIRECT lookup keyed on caller-supplied runId (unlike
35
+ // the POINTER_RE walk above, which only ever sees filenames readdirSync already returned)
36
+ // — without this check a runId containing '/', '..', or backslashes reaches
37
+ // runState.readPointer's `council-${runId}.json` path.join unfiltered and can collapse
38
+ // out of the sessions dir entirely (Task 4 review finding: traversal via unvalidated runId).
39
+ const RUN_ID_RE = /^[a-zA-Z0-9_-]{1,64}$/;
40
+
41
+ function sessionsDir(project) {
42
+ return path.join(project, '.claude', SESSIONS_DIR);
43
+ }
44
+
45
+ function readJson(file) {
46
+ try { return { doc: JSON.parse(fs.readFileSync(file, 'utf-8')) }; }
47
+ catch (err) { return { error: err.message }; }
48
+ }
49
+
50
+ /** Best-effort sort timestamp: run.json createdAt || first stage start || pointer mtime. */
51
+ function startedAtOf(run, pointerPath) {
52
+ if (run) {
53
+ if (run.createdAt) { return run.createdAt; }
54
+ const s0 = Array.isArray(run.stages) && run.stages[0];
55
+ if (s0 && s0.startedAt) { return s0.startedAt; }
56
+ }
57
+ try { return fs.statSync(pointerPath).mtime.toISOString(); } catch { return null; }
58
+ }
59
+
60
+ /**
61
+ * Resolve a council runId to its run dir via the pointer file.
62
+ * Accepts the id with or without the `council-` prefix (v4.0 readPointer parity).
63
+ * ⚠️ DE-ROT (F25): thin ADAPTER over the shipped src/council/run-state.js:134
64
+ * readPointer — that one already strips the prefix (:119) and validates
65
+ * {runId, runDir}, but returns null. This wrapper is the ONLY place that turns
66
+ * null into the {runId, error} row the workspace run list renders (§5.1).
67
+ * @returns {{runId: string, runDir: string} | {runId: string, error: string}}
68
+ */
69
+ function readPointer(project, runId) {
70
+ const id = String(runId).replace(/^council-/, '');
71
+ if (!RUN_ID_RE.test(id)) { return { runId: id, error: 'invalid runId' }; }
72
+ const ptr = runState.readPointer(project, id);
73
+ if (!ptr) { return { runId: id, error: 'pointer missing, unreadable, or invalid' }; }
74
+ return { runId: id, runDir: ptr.runDir };
75
+ }
76
+
77
+ /**
78
+ * @param {string} project absolute project dir (AMICUS_PROJECT)
79
+ * @returns {Array<object>} RunRow[] sorted startedAt desc; error rows inline.
80
+ */
81
+ function scanCouncilRuns(project) {
82
+ let names = [];
83
+ try { names = fs.readdirSync(sessionsDir(project)); } catch { return []; }
84
+ const rows = [];
85
+ for (const name of names) {
86
+ const m = POINTER_RE.exec(name);
87
+ if (!m) { continue; }
88
+ const pointerPath = path.join(sessionsDir(project), name);
89
+ const ptr = readPointer(project, m[1]);
90
+ if (ptr.error) {
91
+ rows.push({ runId: m[1], runDir: null, error: ptr.error, pointerPath, startedAt: startedAtOf(null, pointerPath) });
92
+ continue;
93
+ }
94
+
95
+ // Outer containment fence (third council-review pass): mirrors artifact-guard.js's
96
+ // readRunArtifact / run-detail.js's getRunDetail — same isRealpathContained helper,
97
+ // same check. This feeds the GUI run list (spec §5.1), so a fenced-out pointer MUST
98
+ // degrade to an error row exactly like the dangling-pointer/unreadable-run.json cases
99
+ // below rather than throw — one bad pointer must never blank the whole run list.
100
+ let realProject, realRunDir;
101
+ try {
102
+ realProject = fs.realpathSync(project);
103
+ realRunDir = fs.realpathSync(ptr.runDir);
104
+ } catch (err) {
105
+ rows.push({
106
+ runId: m[1], runDir: ptr.runDir, error: `run dir unreadable: ${err.message}`,
107
+ pointerPath, startedAt: startedAtOf(null, pointerPath),
108
+ });
109
+ continue;
110
+ }
111
+ if (!isRealpathContained(realProject, realRunDir)) {
112
+ rows.push({
113
+ runId: m[1], runDir: ptr.runDir, error: 'run directory escapes project',
114
+ pointerPath, startedAt: startedAtOf(null, pointerPath),
115
+ });
116
+ continue;
117
+ }
118
+
119
+ const r = readJson(path.join(ptr.runDir, 'run.json'));
120
+ if (r.error) {
121
+ rows.push({ runId: m[1], runDir: ptr.runDir, error: `run.json: ${r.error}`, pointerPath, startedAt: startedAtOf(null, pointerPath) });
122
+ continue;
123
+ }
124
+ const run = r.doc;
125
+ const row = {
126
+ runId: run.runId || m[1],
127
+ runDir: ptr.runDir,
128
+ status: run.status || 'unknown',
129
+ startedAt: startedAtOf(run, pointerPath),
130
+ completedAt: run.completedAt || null,
131
+ bench: Array.isArray(run.bench) ? run.bench : [],
132
+ chair: run.chair || null,
133
+ costDisplay: formatCost(run.usage && run.usage.cost),
134
+ tierCounts: null,
135
+ overallVerdict: null,
136
+ };
137
+ const v = readJson(path.join(ptr.runDir, 'verdict.json'));
138
+ if (!v.error && v.doc) {
139
+ row.overallVerdict = v.doc.overallVerdict === undefined ? null : v.doc.overallVerdict;
140
+ row.tierCounts = v.doc.tierCounts || null;
141
+ }
142
+ rows.push(row);
143
+ }
144
+ rows.sort((a, b) => String(b.startedAt || '').localeCompare(String(a.startedAt || '')));
145
+ return rows;
146
+ }
147
+
148
+ module.exports = { scanCouncilRuns, readPointer, POINTER_RE };