amicus 4.5.4 → 4.6.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 (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +71 -0
  3. package/README.md +1 -1
  4. package/commands/council.md +1 -1
  5. package/docs/DISTRIBUTION.md +38 -11
  6. package/docs/usage.md +1 -1
  7. package/package.json +3 -2
  8. package/schemas/council-run.schema.json +20 -0
  9. package/schemas/council-verdict.schema.json +20 -0
  10. package/schemas/doctor.schema.json +23 -1
  11. package/src/cli-council-run-render.js +51 -0
  12. package/src/cli-handlers-council-run.js +45 -44
  13. package/src/cli-handlers-council.js +9 -3
  14. package/src/cli-handlers-doctor.js +16 -37
  15. package/src/cli-handlers-watch.js +1 -1
  16. package/src/cli.js +1 -1
  17. package/src/council/ledger.js +5 -1
  18. package/src/council/report-html.js +16 -1
  19. package/src/council/report.js +25 -1
  20. package/src/council/run-assemble.js +25 -7
  21. package/src/council/run-budget.js +14 -8
  22. package/src/council/run-chair.js +21 -4
  23. package/src/council/run-debate-stage.js +115 -0
  24. package/src/council/run-degrade.js +44 -0
  25. package/src/council/run-finalize.js +18 -3
  26. package/src/council/run-server.js +24 -7
  27. package/src/council/run-stage2.js +10 -2
  28. package/src/council/run-stages.js +23 -21
  29. package/src/council/run.js +39 -67
  30. package/src/council/verdict.js +74 -8
  31. package/src/mcp-council-bench.js +45 -0
  32. package/src/mcp-council-run.js +11 -28
  33. package/src/mcp-server.js +5 -1
  34. package/src/mcp-tools.js +8 -0
  35. package/src/utils/degrade.js +68 -0
  36. package/src/utils/doctor-degrade.js +51 -0
  37. package/src/utils/doctor-electron-mcp-check.js +64 -5
  38. package/src/utils/doctor-engine-check.js +14 -3
  39. package/src/utils/doctor-mcp-checks.js +10 -3
  40. package/src/utils/known-flags.js +2 -1
  41. package/src/utils/remediation-hints.js +5 -3
  42. package/src/utils/result-schema.js +6 -2
  43. package/src/utils/session-index-tmp-sweep.js +2 -1
  44. package/src/workspace/run-scan.js +5 -1
@@ -73,7 +73,7 @@ function evaluateEngineInstalls(d) {
73
73
  * the engine into each via d.repairEngine, then re-report from a fresh scan.
74
74
  * Without d.fix — or when nothing is copy-fixable — returns the plain verdict.
75
75
  * @param {object} d doctor deps (scanEngineInstalls, fix?, repairEngine?)
76
- * @returns {Promise<{id,name,status,message,hint}>}
76
+ * @returns {Promise<{id,name,status,message,hint,fixed?,fixDetail?}>}
77
77
  */
78
78
  async function evaluateEngineMcp(d) {
79
79
  const verdict = evaluateEngineInstalls(d);
@@ -94,11 +94,22 @@ async function evaluateEngineMcp(d) {
94
94
  const after = evaluateEngineInstalls(d); // fresh scan reflects the copies
95
95
  if (after.status === 'ok') {
96
96
  const n = results.length;
97
- return { ...after, message: `${after.message} (self-healed ${n} npx-cache ${plural(n, 'copy', 'copies')})` };
97
+ return {
98
+ ...after,
99
+ message: `${after.message} (self-healed ${n} npx-cache ${plural(n, 'copy', 'copies')})`,
100
+ fixed: true,
101
+ fixDetail: `copied the engine into ${n} npx-cache ${plural(n, 'copy', 'copies')}`,
102
+ };
98
103
  }
99
104
  const failed = results.filter((r) => !r.repaired)
100
105
  .map((r) => `${r.pkgDir}${r.reason ? ` — ${r.reason}` : ''}`).join('; ');
101
- return { ...after, message: `${after.message}; self-heal incomplete: ${failed}` };
106
+ // Partial credit: some copies healed even though the check overall is still
107
+ // not 'ok' — flag it ONLY when >=1 repair actually succeeded (#84-style rule).
108
+ const healed = results.filter((r) => r.repaired).length;
109
+ const fixFields = healed > 0
110
+ ? { fixed: true, fixDetail: `copied the engine into ${healed} npx-cache ${plural(healed, 'copy', 'copies')}` }
111
+ : {};
112
+ return { ...after, message: `${after.message}; self-heal incomplete: ${failed}`, ...fixFields };
102
113
  }
103
114
 
104
115
  module.exports = { evaluateEngineInstalls, evaluateEngineMcp };
@@ -68,14 +68,21 @@ function evaluateLegacyMcpEntry(d) {
68
68
  }
69
69
  if (d.fix) {
70
70
  const removed = (d.migrateLegacyMcpEntries() || []).filter(r => r.result === 'removed');
71
+ // Structured fix outcome (v4.6 Plan 3 Task 3): a repaired row carries
72
+ // fixed/fixDetail whenever ANY entry was actually removed, even on the
73
+ // partial-failure path below — a genuine no-op (removed.length === 0)
74
+ // stays unflagged.
75
+ const fixFields = removed.length > 0
76
+ ? { fixed: true, fixDetail: `removed the duplicate legacy 'sidecar' entry from ${removed.map(r => r.target).join(', ')}` }
77
+ : {};
71
78
  if (removed.length >= dupes.length) {
72
79
  const message = `removed legacy entry from: ${removed.map(r => r.target).join(', ')}`;
73
80
  return unreadableNote
74
- ? { id, name, status: 'warn', message: `${message}; ${unreadableNote}`, hint: HINTS.removeLegacySidecar }
75
- : { id, name, status: 'ok', message, hint: null };
81
+ ? { id, name, status: 'warn', message: `${message}; ${unreadableNote}`, hint: HINTS.removeLegacySidecar, ...fixFields }
82
+ : { id, name, status: 'ok', message, hint: null, ...fixFields };
76
83
  }
77
84
  const message = `removed ${removed.length}/${dupes.length} duplicate(s) — could not update every config`;
78
- return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
85
+ return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar, ...fixFields };
79
86
  }
80
87
  const message = `duplicate 'sidecar' entry in ${dupes.map(e => e.target).join(', ')} — doubles the MCP tool list`;
81
88
  return { id, name, status: 'warn', message: unreadableNote ? `${message}; ${unreadableNote}` : message, hint: HINTS.removeLegacySidecar };
@@ -29,7 +29,7 @@
29
29
  * Real flags that appear in NO usage block. Rejecting any of these would break
30
30
  * working callers, so they are enumerated deliberately rather than derived.
31
31
  *
32
- * ⚠️ The first four are spawned by the MCP server onto its own CLI children
32
+ * ⚠️ These are spawned by the MCP server onto its own CLI children
33
33
  * (src/mcp-server.js, src/mcp-council-run.js). They are not user-facing and are
34
34
  * intentionally undocumented — but they are on the argv of every MCP-launched
35
35
  * run, so rejecting them would break the entire MCP surface.
@@ -39,6 +39,7 @@ const INTERNAL_FLAGS = new Set([
39
39
  'run-id', // MCP → `council run`: pins the child's run id
40
40
  'council-name', // MCP → `council run`: preset name for ledger attribution
41
41
  'cowork-process', // MCP → `start`: Cowork process handle for context capture
42
+ 'dropped-members', // MCP → 'council run': per-member preset drops as JSON (v4.6 Plan 4)
42
43
 
43
44
  // User-facing but undocumented, and read by real handlers today. Listed so the
44
45
  // rejection is a bug fix and not a silent removal of working behaviour; if any
@@ -33,7 +33,7 @@ const REMEDIATION_HINTS = Object.freeze({
33
33
  */
34
34
  reinstallEngineAv:
35
35
  'npm install -g amicus (a transient install error can roll back the engine binaries — re-run, or: npm cache clean --force && npm install -g amicus). '
36
- + 'If your antivirus (e.g. Windows Defender) quarantined opencode.exe, allow-list it first, then reinstall.',
36
+ + 'If your antivirus (e.g. Windows Defender) quarantined opencode.exe — unverified, but a known cause — allow-list it first, then reinstall.',
37
37
 
38
38
  /**
39
39
  * Runtime server-start failure when the opencode engine binary does not
@@ -41,8 +41,10 @@ const REMEDIATION_HINTS = Object.freeze({
41
41
  * spawn ENOENT — surfaced by startServer (the missing-binary boundary).
42
42
  */
43
43
  engineMissing:
44
- 'OpenCode engine binary not found — it was likely skipped during install or quarantined by antivirus. '
45
- + 'Run "amicus doctor", reinstall with "npm i -g amicus", and allow-list opencode.exe in your antivirus.',
44
+ 'OpenCode engine binary not found — the cause was not verified. Common causes (unverified): '
45
+ + 'an install that skipped or rolled back the engine packages, or antivirus quarantine of opencode.exe. '
46
+ + 'Run "amicus doctor" to check the actual install, reinstall with "npm i -g amicus", '
47
+ + 'and allow-list opencode.exe in your antivirus if quarantine was the cause.',
46
48
 
47
49
  /** Electron absent — reinstall to add the interactive GUI (headless still works). */
48
50
  reinstallElectron: 'npm install -g amicus (reinstall to add Electron)',
@@ -209,9 +209,10 @@ function buildAuditDoc({ stale, catalogAvailable, gatewayFindings = [] }) {
209
209
 
210
210
  /**
211
211
  * Build a doctor health-check document (`doctor --json`).
212
- * @param {{version: string, timestamp: string, checks: Array<{id,name,status,message,hint}>}} opts
212
+ * @param {{version: string, timestamp: string, checks: Array<{id,name,status,message,hint}>,
213
+ * degrades?: Array<{kind,channel,what,why,effect,remedy?,data?}>}} opts
213
214
  */
214
- function buildDoctorDoc({ version, timestamp, checks }) {
215
+ function buildDoctorDoc({ version, timestamp, checks, degrades }) {
215
216
  return {
216
217
  schemaVersion: SCHEMA_VERSION,
217
218
  type: 'doctor',
@@ -219,6 +220,9 @@ function buildDoctorDoc({ version, timestamp, checks }) {
219
220
  version,
220
221
  timestamp,
221
222
  checks,
223
+ // v4.6 Plan 3 (spec §4/§6): the shared-vocabulary surface. Additive and
224
+ // OPTIONAL — present only when a check failed or --fix repaired something.
225
+ ...(degrades && degrades.length ? { degrades } : {}),
222
226
  };
223
227
  }
224
228
 
@@ -70,7 +70,8 @@ function evaluateSessionIndexTmpSweep(d) {
70
70
  }
71
71
  const remaining = files.length - swept;
72
72
  if (remaining === 0) {
73
- return { id, name, status: 'ok', message: `swept ${swept} orphaned tmp file(s)`, hint: null };
73
+ const fixFields = swept > 0 ? { fixed: true, fixDetail: `swept ${swept} orphaned session-index tmp file(s)` } : {};
74
+ return { id, name, status: 'ok', message: `swept ${swept} orphaned tmp file(s)`, hint: null, ...fixFields };
74
75
  }
75
76
  return { id, name, status: 'warn', message: `swept ${swept}, ${remaining} remaining (too fresh or unremovable)`, hint: HINTS.sweepSessionIndexTmp };
76
77
  }
@@ -70,7 +70,11 @@ function readPointer(project, runId) {
70
70
  const id = String(runId).replace(/^council-/, '');
71
71
  if (!RUN_ID_RE.test(id)) { return { runId: id, error: 'invalid runId' }; }
72
72
  const ptr = runState.readPointer(project, id);
73
- if (!ptr) { return { runId: id, error: 'pointer missing, unreadable, or invalid' }; }
73
+ if (!ptr) {
74
+ return { runId: id, error: 'pointer missing — run pointers live under the LAUNCH directory '
75
+ + '(where `council run` was invoked), not --out-dir. If this run used --out-dir, point '
76
+ + '--project at the launch directory instead.' };
77
+ }
74
78
  return { runId: id, runDir: ptr.runDir };
75
79
  }
76
80