amicus 4.6.3 → 4.7.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 (86) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +283 -0
  3. package/README.md +19 -7
  4. package/bin/amicus.js +31 -0
  5. package/docs/ROADMAP.md +143 -36
  6. package/docs/configuration.md +51 -3
  7. package/docs/council.md +63 -10
  8. package/docs/doc-system.md +8 -7
  9. package/docs/schemas.md +10 -1
  10. package/docs/troubleshooting.md +27 -1
  11. package/docs/usage.md +59 -13
  12. package/electron/workspace-ui/index.html +3 -0
  13. package/electron/workspace-ui/live-model.js +52 -14
  14. package/electron/workspace-ui/workspace-app.js +14 -3
  15. package/electron/workspace-ui/workspace-lazy.js +233 -0
  16. package/electron/workspace-ui/workspace-matrix.js +12 -1
  17. package/electron/workspace-ui/workspace-panels.js +24 -171
  18. package/electron/workspace-ui/workspace-render.js +6 -2
  19. package/electron/workspace-ui/workspace-seats.js +68 -0
  20. package/electron/workspace-ui/workspace.css +6 -0
  21. package/package.json +5 -2
  22. package/schemas/council-run.schema.json +1 -0
  23. package/schemas/council-stats.schema.json +9 -1
  24. package/schemas/run.schema.json +2 -1
  25. package/schemas/spend.schema.json +1 -1
  26. package/schemas/wave.schema.json +2 -1
  27. package/skills/second-opinion/MANUAL-ORCHESTRATION.md +12 -0
  28. package/skills/second-opinion/MODEL-NOTES.md +5 -4
  29. package/skills/sidecar/SKILL.md +7 -2
  30. package/src/cli-council-run-bench.js +86 -0
  31. package/src/cli-handlers-council-run.js +65 -81
  32. package/src/cli-handlers-council.js +17 -5
  33. package/src/cli-handlers-fanout.js +179 -0
  34. package/src/cli-handlers-pack.js +24 -10
  35. package/src/cli-handlers-run.js +19 -161
  36. package/src/cli-template-args.js +48 -0
  37. package/src/cli.js +39 -46
  38. package/src/council/debate.js +89 -10
  39. package/src/council/ledger.js +72 -11
  40. package/src/council/report.js +17 -6
  41. package/src/council/run-assemble.js +15 -3
  42. package/src/council/run-budget.js +2 -2
  43. package/src/council/run-chair.js +61 -5
  44. package/src/council/run-debate.js +51 -67
  45. package/src/council/run-launch.js +9 -2
  46. package/src/council/run-retry.js +4 -1
  47. package/src/council/run-stage1-launch.js +94 -0
  48. package/src/council/run-stage2.js +25 -4
  49. package/src/council/run-stages.js +79 -86
  50. package/src/council/run-state.js +10 -2
  51. package/src/council/run.js +26 -2
  52. package/src/council/tally.js +6 -2
  53. package/src/mcp-council-awareness.js +1 -0
  54. package/src/mcp-council-bench.js +4 -0
  55. package/src/mcp-council-run.js +10 -0
  56. package/src/mcp-server.js +114 -54
  57. package/src/mcp-tools.js +12 -5
  58. package/src/pack/pack-cli.js +1 -1
  59. package/src/pack/pack-forward.js +12 -4
  60. package/src/pack/pack-resolve.js +3 -0
  61. package/src/pack/pack-store.js +20 -3
  62. package/src/pack/pack-validate.js +5 -1
  63. package/src/sidecar/budget.js +38 -4
  64. package/src/sidecar/fanout-budget.js +1 -2
  65. package/src/sidecar/fanout-leg-fallback.js +7 -3
  66. package/src/sidecar/fanout-wave-io.js +13 -1
  67. package/src/sidecar/fanout.js +11 -9
  68. package/src/sidecar/list-limit.js +50 -0
  69. package/src/sidecar/list-search.js +69 -0
  70. package/src/sidecar/read.js +90 -5
  71. package/src/sidecar/start-metadata.js +58 -0
  72. package/src/sidecar/start.js +8 -43
  73. package/src/sidecar/workspace-auto-open.js +2 -2
  74. package/src/spend-query.js +2 -1
  75. package/src/template/apply.js +7 -4
  76. package/src/template/render.js +6 -2
  77. package/src/template/store.js +1 -1
  78. package/src/utils/cli-preflight.js +27 -1
  79. package/src/utils/config.js +15 -0
  80. package/src/utils/result-schema-rebuild.js +1 -0
  81. package/src/utils/result-schema.js +6 -1
  82. package/src/utils/session-index-tmp-sweep.js +18 -3
  83. package/src/utils/session-index.js +1 -0
  84. package/src/utils/session-metadata-tmp-sweep.js +24 -4
  85. package/src/utils/spend-ledger.js +11 -4
  86. package/src/utils/validators.js +16 -0
@@ -29,6 +29,14 @@
29
29
  * symlinked taskId or subagents directory could otherwise be traversed and
30
30
  * have files unlinked through the link, effectively outside the sessions
31
31
  * root; lstat closes that off at zero cost.
32
+ *
33
+ * Consequence of the SR-3 isFile() gate (listTmpIn, below): a SYMLINK whose
34
+ * basename matches the tmp pattern is now excluded from the list entirely —
35
+ * neither swept nor reported. Before SR-3 it was swept (unlink removes the
36
+ * link, never the target — a safe success). Deliberate: this module's
37
+ * never-follow policy applies to the entries it unlinks too. Note the sibling
38
+ * session-index-tmp-sweep.js diverges here — it uses statSync, so a
39
+ * symlink-to-a-file with the matching name IS still swept there.
32
40
  */
33
41
 
34
42
  const fs = require('fs');
@@ -38,7 +46,14 @@ const HINTS = require('./remediation-hints');
38
46
  /** Files older than this survive to the next --fix, never a live writer's ms-lived tmp. */
39
47
  const AGE_THRESHOLD_MS = 60 * 1000;
40
48
 
41
- /** The cwd-scoped sessions root: <cwd>/.claude/amicus_sessions. */
49
+ /**
50
+ * The cwd-scoped sessions root: <cwd>/.claude/amicus_sessions.
51
+ * Reads process.cwd() directly, NOT doctor's injected getCwd
52
+ * (cli-handlers-doctor.js's realDeps().getCwd) — the
53
+ * listSessionMetadataTmpFiles/unlinkSessionMetadataTmp deps are wired
54
+ * argument-free in that same realDeps(), so that seam does not reach here.
55
+ * Thread cwd through those deps if a `doctor --cwd <dir>` mode ever lands.
56
+ */
42
57
  function sessionsRoot() {
43
58
  const { SESSIONS_DIR } = require('../session-manager');
44
59
  return path.join(process.cwd(), '.claude', SESSIONS_DIR);
@@ -56,9 +71,9 @@ function listTmpIn(dir, root) {
56
71
  return entries
57
72
  .filter(isMetadataTmp)
58
73
  .map((basename) => {
59
- let mtimeMs = null;
60
- try { mtimeMs = fs.lstatSync(path.join(dir, basename)).mtimeMs; } catch { /* raced away — skip below */ }
61
- return { name: path.relative(root, path.join(dir, basename)), mtimeMs };
74
+ let st = null;
75
+ try { st = fs.lstatSync(path.join(dir, basename)); } catch { /* raced away */ }
76
+ return { name: path.relative(root, path.join(dir, basename)), mtimeMs: st && st.isFile() ? st.mtimeMs : null };
62
77
  })
63
78
  .filter((f) => f.mtimeMs !== null);
64
79
  }
@@ -107,6 +122,11 @@ function unlinkSessionMetadataTmp(name) {
107
122
  * wraps this in guard() the same way it wires the sibling sessions-index-tmp check.
108
123
  * @param {{listSessionMetadataTmpFiles: () => Array<{name:string, mtimeMs:number}>,
109
124
  * fix?: boolean, now: () => number, unlinkSessionMetadataTmp: (name: string) => void}} d
125
+ * The four `message` strings below are byte-identical to the index sibling's
126
+ * (session-index-tmp-sweep.js's evaluateSessionIndexTmpSweep, same four
127
+ * ok/warn returns) on purpose — `id`/`name` and the `fixDetail` wording are
128
+ * the only disambiguators between the two rows. Reword one side and the
129
+ * pairing silently breaks: reword both, or neither.
110
130
  */
111
131
  function evaluateSessionMetadataTmpSweep(d) {
112
132
  const id = 'session-metadata-tmp'; const name = 'Session metadata tmp files';
@@ -55,13 +55,17 @@ const SPEND_LEDGER_FILE = 'spend-ledger.jsonl';
55
55
  * @param {string} [opts.councilName] council name (additive attribution)
56
56
  * @param {string} [opts.project] project directory (additive attribution)
57
57
  * @param {string} [opts.gateway] resolved gateway ('direct'|'openrouter'|'local', additive attribution)
58
+ * @param {string} [opts.tag] user-chosen tag (v4.7 F8 D16, additive attribution) —
59
+ * nullable dim, same null-not-absent convention as councilRunId/project below;
60
+ * deliberately the OPPOSITE convention from createSessionMetadata's tag (D13,
61
+ * absent-not-null) — see the dim-list comment below.
58
62
  * @param {number} [opts.attempt] fallback attempt count (omitted if absent)
59
63
  * @param {string} [opts.substitutedFor] substituted model (omitted if absent)
60
64
  * @param {string} [opts.retryOfWaveId] wave id being retried (omitted if absent)
61
65
  * @param {{dir?:string}} [ctx] test seam — dir overrides getConfigDir()
62
66
  */
63
67
  function appendSpend({ taskId, waveId, model, mode, usage,
64
- op, status, councilRunId, councilName, project, gateway,
68
+ op, status, councilRunId, councilName, project, gateway, tag,
65
69
  attempt, substitutedFor, retryOfWaveId }, ctx = {}) {
66
70
  if (!usage) { return; }
67
71
  try {
@@ -76,15 +80,18 @@ function appendSpend({ taskId, waveId, model, mode, usage,
76
80
  mode: mode || null,
77
81
  tokens: usage.tokens || null,
78
82
  cost: usage.cost || null,
79
- // v4.3 additive attribution (spec 7.1). Nullable dimensions default to
80
- // null (so a row is always groupable); linkage fields are OMITTED unless
81
- // present (they only exist on fallback/retry rows).
83
+ // v4.3 additive attribution (spec 7.1), extended v4.7 F8 D16 with `tag`.
84
+ // Nullable dimensions (op/status/councilRunId/councilName/project/
85
+ // gateway/tag) default to null (so a row is always groupable); linkage
86
+ // fields are OMITTED unless present (they only exist on fallback/retry
87
+ // rows).
82
88
  op: op || null,
83
89
  status: status || null,
84
90
  councilRunId: councilRunId || null,
85
91
  councilName: councilName || null,
86
92
  project: project || null,
87
93
  gateway: gateway || null,
94
+ tag: tag || null,
88
95
  };
89
96
  if (attempt !== undefined) { row.attempt = attempt; }
90
97
  if (substitutedFor !== undefined) { row.substitutedFor = substitutedFor; }
@@ -24,6 +24,20 @@ const { PROVIDER_KEY_MAP } = require('./provider-registry');
24
24
  /** Task ID format: alphanumeric, hyphens, underscores, 1-64 chars */
25
25
  const TASK_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
26
26
 
27
+ const TAG_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
28
+ /**
29
+ * v4.7 F8 (D13): tag validation REJECTS (unlike sanitizeCouncilName, which
30
+ * cleans) — a stored tag is a user-chosen search key, so silent truncation
31
+ * or charset-stripping would make `--search`/`--group-by tag` miss it.
32
+ * Non-string guards the valueless `--tag` parse (cli.js turns it into true).
33
+ */
34
+ function validateTag(value) {
35
+ if (typeof value !== 'string' || !TAG_PATTERN.test(value)) {
36
+ return { ok: false, error: 'Invalid --tag: 1-64 chars, letters/digits/_/- only' };
37
+ }
38
+ return { ok: true, tag: value };
39
+ }
40
+
27
41
  /**
28
42
  * Validate a task ID format (safe for use in file paths)
29
43
  * @param {string} taskId
@@ -249,6 +263,8 @@ module.exports = {
249
263
  MODEL_THINKING_SUPPORT,
250
264
  TASK_ID_PATTERN,
251
265
  validateTaskId,
266
+ TAG_PATTERN,
267
+ validateTag,
252
268
  safeSessionDir,
253
269
  validatePromptContent,
254
270
  validateCwdPath,