@event4u/agent-config 14.2.0 → 14.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 (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +33 -252
  3. package/agents/templates/.ai-council.yml.example +35 -0
  4. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +83 -12
  5. package/dist/agent-src/rules/roadmap-progress-sync.md +48 -4
  6. package/dist/agent-src/scripts/resume_probe.ts +154 -9
  7. package/dist/agent-src/scripts/roadmap_gates.ts +210 -6
  8. package/dist/agent-src/skills/roadmap-management/SKILL.md +9 -4
  9. package/dist/agent-src/templates/agent-settings.md +70 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  12. package/dist/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  13. package/dist/cli/python/workspace_drive.js +8 -3
  14. package/dist/cli/python/workspace_drive.js.map +1 -1
  15. package/dist/cli/python/workspace_hosts.js +16 -12
  16. package/dist/cli/python/workspace_hosts.js.map +1 -1
  17. package/dist/cli/registry.js +2 -0
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/cli-delegate/{chunk-F3YUF7SA.js → chunk-3UCJWMMP.js} +1 -1
  20. package/dist/cli-delegate/chunk-F7DQWYR5.js +69 -0
  21. package/dist/cli-delegate/{chunk-7PZ4FSZB.js → chunk-FBKL6JWT.js} +61 -2
  22. package/dist/cli-delegate/{chunk-MNWRDMR5.js → chunk-PKOBQADP.js} +1 -1
  23. package/dist/cli-delegate/cmd_conformance.js +26 -11
  24. package/dist/cli-delegate/cmd_doctor.js +2 -2
  25. package/dist/cli-delegate/cmd_handoff.js +3 -3
  26. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  27. package/dist/cli-delegate/cmd_settings_set.js +17 -3
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +4 -4
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/dispatch.js +3754 -1721
  38. package/dist/mcp/registry-manifest.json +2 -2
  39. package/dist/mcp/server.json +2 -2
  40. package/dist/mcp/server.mjs +1070 -408
  41. package/dist/scripts/_lib/host_launch.js +66 -0
  42. package/dist/scripts/_lib/host_launch.js.map +1 -0
  43. package/dist/server/io/yamlIO.js +175 -3
  44. package/dist/server/io/yamlIO.js.map +1 -1
  45. package/dist/server/routes/wizard.js +20 -1
  46. package/dist/server/routes/wizard.js.map +1 -1
  47. package/dist/shared/settingsConsent.js +25 -0
  48. package/dist/shared/settingsConsent.js.map +1 -1
  49. package/docs/contracts/settings-classes.md +11 -0
  50. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +2 -1
  51. package/package.json +1 -1
  52. package/src/agent-src/scripts/resume_probe.ts +154 -9
  53. package/src/agent-src/scripts/roadmap_gates.ts +210 -6
  54. package/src/agent-src/templates/scripts/telemetry/remote.ts +571 -0
  55. package/src/agent-src/templates/scripts/telemetry/settings.ts +222 -44
  56. package/src/config/estate-count-budget.json +127 -0
  57. package/src/config/evaluator-budgets.json +4 -4
  58. package/src/config/gate-coverage.yml +32 -4
  59. package/src/config/gate-violation-baselines.json +63 -63
  60. package/src/config/gitignore-block.txt +5 -0
  61. package/src/config/hook-latency-budget.json +40 -1
  62. package/src/config/hook-token-budget.json +16 -0
  63. package/src/config/rule-stub-ceilings.json +11 -3
  64. package/src/scripts/_dispatch.bash +42 -1
  65. package/src/scripts/_lib/council_fallback_posture.ts +81 -0
  66. package/src/scripts/_lib/council_fallback_wiring.ts +126 -0
  67. package/src/scripts/_lib/council_settings_block.ts +152 -0
  68. package/src/scripts/_lib/headless_invocation.ts +285 -0
  69. package/src/scripts/_lib/host_launch.ts +72 -0
  70. package/src/scripts/_lib/map_to_object.ts +16 -0
  71. package/src/scripts/_lib/roadmap_checkboxes.ts +88 -0
  72. package/src/scripts/_lib/run_checkpoint.ts +409 -0
  73. package/src/scripts/_lib/session_register.ts +32 -1
  74. package/src/scripts/_lib/stdin.ts +35 -1
  75. package/src/scripts/_lib/turn_end_refusals.ts +12 -4
  76. package/src/scripts/_lib/unattended_guard.ts +371 -0
  77. package/src/scripts/ai_council/config.ts +27 -1
  78. package/src/scripts/ai_council/events_log.ts +12 -1
  79. package/src/scripts/ai_council/fallback_config.ts +114 -0
  80. package/src/scripts/ai_council/mid_flight_fallback.ts +295 -0
  81. package/src/scripts/ai_council/orchestrator.ts +251 -292
  82. package/src/scripts/ai_council/py_parity.ts +139 -0
  83. package/src/scripts/ai_council/response_render.ts +99 -0
  84. package/src/scripts/ai_council/spend_gate.ts +123 -0
  85. package/src/scripts/ai_council/transport_resolver.ts +81 -13
  86. package/src/scripts/bench_hook_injection.ts +118 -7
  87. package/src/scripts/bench_hook_latency.ts +442 -16
  88. package/src/scripts/check_estate_count.ts +924 -0
  89. package/src/scripts/compile_hook_manifest.ts +67 -0
  90. package/src/scripts/council_cli.ts +100 -125
  91. package/src/scripts/decision_memo.ts +325 -0
  92. package/src/scripts/hook_manifest.json +1 -0
  93. package/src/scripts/hook_manifest.yaml +172 -25
  94. package/src/scripts/hooks/concern_registry.ts +6 -0
  95. package/src/scripts/hooks/dispatch_hook.ts +183 -10
  96. package/src/scripts/hooks/dispatch_issues.ts +26 -0
  97. package/src/scripts/hooks/end_review_nudge_hook.ts +97 -24
  98. package/src/scripts/hooks/hook_stdin.ts +111 -5
  99. package/src/scripts/hooks/host_semantics.ts +28 -0
  100. package/src/scripts/hooks/injection_budget.ts +731 -0
  101. package/src/scripts/hooks/payload_stub.ts +446 -0
  102. package/src/scripts/hooks/run_continuation_hook.ts +683 -0
  103. package/src/scripts/hooks/session_eol_hook.ts +52 -0
  104. package/src/scripts/hooks/telemetry_usage_hook.ts +339 -0
  105. package/src/scripts/hooks/tool_result_bytes_hook.ts +27 -0
  106. package/src/scripts/hooks/turn_end_gate_hook.ts +45 -7
  107. package/src/scripts/hooks_doctor.ts +16 -2
  108. package/src/scripts/interruption_report.ts +330 -13
  109. package/src/scripts/lint_hook_manifest.ts +167 -0
  110. package/src/scripts/roadmap_progress_hook.ts +254 -46
  111. package/src/scripts/run_supervise.ts +623 -0
  112. package/src/scripts/session_register_hook.ts +146 -12
  113. package/src/scripts/sessions_cli.ts +18 -6
  114. package/src/scripts/telemetry_disclosure_hook.ts +244 -0
  115. package/src/server/io/yamlIO.ts +167 -3
  116. package/src/shared/settingsConsent.ts +49 -2
  117. package/dist/cli-delegate/chunk-MMJL4TIX.js +0 -18
@@ -34,6 +34,7 @@
34
34
  */
35
35
 
36
36
  import { spawnSync } from "node:child_process";
37
+ import * as crypto from "node:crypto";
37
38
  import fs from "node:fs";
38
39
  import path from "node:path";
39
40
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -41,9 +42,146 @@ import { fileURLToPath, pathToFileURL } from "node:url";
41
42
  import { hardenedSpawnEnv } from "./_lib/spawn_env.js";
42
43
  import { log_dispatch_issue } from "./hooks/dispatch_issues.js";
43
44
  import { readHookStdin } from "./hooks/hook_stdin.js";
45
+ import { atomic_write_json } from "./hooks/state_io.js";
44
46
 
45
47
  export const REPLAY_ENV_VAR = "AGENT_CONFIG_REPLAY";
46
48
 
49
+ /**
50
+ * Debounce ledger (road-to-per-turn-hook-economy step 3.1 / D-3).
51
+ *
52
+ * The hook used to re-shell the regenerator through tsx on EVERY roadmap write.
53
+ * A cold tsx start per write, in the roadmap-heavy workflow this repo runs, is
54
+ * the second of the two spawns that escape the in-process concern table.
55
+ *
56
+ * It now records the touched repo roots and regenerates ONCE at turn end.
57
+ * Two properties make that safe rather than merely cheaper:
58
+ *
59
+ * - **The ledger lives under `consumer_root`, not under the target root.** A
60
+ * touched root may be a sibling worktree, and at `stop` the payload carries no
61
+ * roadmap path — so a marker written beside the edited file would be
62
+ * unfindable at flush time. `--project-dir` is the same value on both events,
63
+ * which makes one ledger per consumer the only shape that closes.
64
+ * - **A ledger write that fails regenerates immediately instead.** Losing the
65
+ * dashboard-stays-in-sync guarantee silently is worse than paying the spawn,
66
+ * so the debounce degrades to the old behaviour rather than to a no-op.
67
+ *
68
+ * Flushed by `stop` AND `session_end`, so the worst case is a regeneration
69
+ * deferred to turn end (risk 4) and never a dropped one. The per-write guarantee
70
+ * was never a stated contract; session granularity is.
71
+ */
72
+ export const DIRTY_LEDGER_REL = path.join(
73
+ "agents",
74
+ "runtime",
75
+ "state",
76
+ "roadmap-progress",
77
+ "dirty",
78
+ );
79
+
80
+ /** Events that flush the ledger rather than appending to it. */
81
+ export const FLUSH_EVENTS: ReadonlySet<string> = new Set([
82
+ "stop",
83
+ "session_end",
84
+ // The dispatcher envelope carries the agent-config event id; a bare host
85
+ // payload carries the native name, and the hook accepts either shape on the
86
+ // write path already.
87
+ "Stop",
88
+ "SessionEnd",
89
+ "sessionEnd",
90
+ // Native aliases `hook_manifest.yaml` maps onto those two events. Missing them
91
+ // was a real gap rather than a completeness nit: the write path keys on
92
+ // `tool_name`, not on the event, so a host whose turn end arrives under one of
93
+ // these names accumulated dirty roots that nothing ever flushed. Found by the
94
+ // R2 review of the commit that introduced this ledger.
95
+ "TaskComplete",
96
+ "TaskCancel",
97
+ "AfterAgent",
98
+ ]);
99
+
100
+ function _ledger_dir(consumer_root: string): string {
101
+ return path.join(consumer_root, DIRTY_LEDGER_REL);
102
+ }
103
+
104
+ /**
105
+ * One entry file per root, named from a hash of the root path.
106
+ *
107
+ * **This shape is the concurrency fix, not a style choice.** The first version
108
+ * kept a single JSON array and did a read-modify-write on it, which is a
109
+ * lost-update: two `post_tool_use` dispatches from parallel tool calls in one
110
+ * turn both read `[]`, the second write drops the first root, and `mark_dirty`
111
+ * still returns `true` — so the inline-regeneration fallback never fires and the
112
+ * dashboard goes silently stale, which is exactly what the debounce exists to
113
+ * prevent. Found by the R2 review of the commit that introduced it.
114
+ *
115
+ * Independent per-root files remove the race by construction rather than by
116
+ * holding a lock across a read and a write: concurrent dispatches touch
117
+ * different paths, and the same root twice is the same path — so N writes to one
118
+ * repo still leave ONE entry.
119
+ */
120
+ function _entry_path(consumer_root: string, root: string): string {
121
+ const digest = crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
122
+ return path.join(_ledger_dir(consumer_root), `${digest}.json`);
123
+ }
124
+
125
+ /** Read the ledger; a missing directory or an unparseable entry reads as empty. */
126
+ export function read_dirty_roots(consumer_root: string): string[] {
127
+ let names: string[];
128
+ try {
129
+ names = fs.readdirSync(_ledger_dir(consumer_root));
130
+ } catch {
131
+ return [];
132
+ }
133
+ const roots: string[] = [];
134
+ for (const name of names.sort()) {
135
+ if (!name.endsWith(".json")) continue;
136
+ try {
137
+ const raw = fs.readFileSync(path.join(_ledger_dir(consumer_root), name), "utf8");
138
+ const parsed = JSON.parse(raw) as unknown;
139
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
140
+ const root = (parsed as { root?: unknown }).root;
141
+ if (typeof root === "string" && root.trim() !== "") roots.push(root);
142
+ } catch {
143
+ // A torn or hand-edited entry is dropped, never fatal.
144
+ }
145
+ }
146
+ return Array.from(new Set(roots)).sort();
147
+ }
148
+
149
+ /**
150
+ * Record each root as its own entry. Returns false when any write failed — the
151
+ * caller then regenerates inline, so a broken state directory costs latency
152
+ * rather than a silently stale dashboard.
153
+ *
154
+ * Writes go through `atomic_write_json` (tmp → rename under the dispatcher
155
+ * lock), which `hook-architecture-v1` requires for everything written under
156
+ * `agents/runtime/state/`. That call is a no-op under `AGENT_CONFIG_REPLAY=1`,
157
+ * which is why the replay guard stays in the caller rather than relying on it.
158
+ */
159
+ export function mark_dirty(consumer_root: string, roots: readonly string[]): boolean {
160
+ if (roots.length === 0) return true;
161
+ let wrote = 0;
162
+ for (const root of roots) {
163
+ try {
164
+ atomic_write_json(_entry_path(consumer_root, root), {
165
+ root,
166
+ marked_at: new Date().toISOString(),
167
+ });
168
+ wrote += 1;
169
+ } catch {
170
+ // A partial write still needs the caller's inline fallback.
171
+ }
172
+ }
173
+ return wrote === roots.length;
174
+ }
175
+
176
+ /** Drop every entry. A missing directory is success — flush is idempotent. */
177
+ export function clear_dirty(consumer_root: string): void {
178
+ try {
179
+ fs.rmSync(_ledger_dir(consumer_root), { recursive: true, force: true });
180
+ } catch {
181
+ // observability never breaks the hook
182
+ }
183
+ }
184
+
47
185
  // Tools whose successful execution can write to a roadmap file. We keep
48
186
  // the list explicit so an unknown tool name (e.g. a new MCP tool that
49
187
  // happens to mention a roadmap path in its input) does not trigger a
@@ -322,6 +460,59 @@ function _resolve_tsx_invocation(scriptPath: string): { command: string; args: s
322
460
  return { command: "npx", args: ["tsx", scriptPath] };
323
461
  }
324
462
 
463
+ /**
464
+ * Run the regenerator once per root. Extracted by step 3.1 so the write path
465
+ * and the flush path share ONE implementation — a debounce whose flush
466
+ * regenerated differently from the per-write path would be a second code path
467
+ * nobody compares.
468
+ */
469
+ function _regenerate(roots: Iterable<string>, verbose: boolean): void {
470
+ for (const root of roots) {
471
+ const script = _resolve_regenerator(root);
472
+ if (script === null) {
473
+ // Phase 1 of road-to-hooks-actually-fire-in-consumers: log dispatch
474
+ // issue directly (this hook runs as a subprocess from the universal
475
+ // dispatcher; routing through the dispatcher would add latency for
476
+ // no benefit).
477
+ try {
478
+ log_dispatch_issue(
479
+ root,
480
+ "roadmap-progress",
481
+ "prerequisite_missing",
482
+ "update_roadmap_progress.ts not found at any of: " +
483
+ ".augment/scripts/, dist/agent-src/scripts/, " +
484
+ "src/agent-src/scripts/",
485
+ "./agent-config hooks:install --regen " + "(or ./agent-config init)",
486
+ );
487
+ } catch {
488
+ // observability never breaks the hook
489
+ }
490
+ if (verbose) {
491
+ process.stderr.write(
492
+ `roadmap-progress-hook: regenerator not found for ${root}, skipping\n`,
493
+ );
494
+ }
495
+ continue;
496
+ }
497
+
498
+ try {
499
+ // The regenerator is a `.ts` script; run it through tsx (resolved from
500
+ // a node_modules/.bin found by walking up from the regenerator's dir,
501
+ // falling back to `npx tsx`), mirroring the dispatcher's `.ts` concerns.
502
+ // `--repo-root` + cwd pin the regeneration to the edited file's own repo.
503
+ const inv = _resolve_tsx_invocation(script);
504
+ spawnSync(inv.command, [...inv.args, "--repo-root", root], {
505
+ cwd: root,
506
+ stdio: ["ignore", "ignore", "ignore"],
507
+ timeout: 30000,
508
+ env: hardenedSpawnEnv(),
509
+ });
510
+ } catch {
511
+ // never propagate regenerator failures into the agent loop
512
+ }
513
+ }
514
+ }
515
+
325
516
  export function run(
326
517
  stdin_text: string,
327
518
  options: { consumer_root: string; verbose?: boolean },
@@ -341,8 +532,18 @@ export function run(
341
532
  }
342
533
  }
343
534
 
344
- // Unwrap dispatcher envelope (Phase 7.3, hook-architecture-v1.md).
535
+ // Unwrap dispatcher envelope (Phase 7.3, hook-architecture-v1.md). The
536
+ // envelope's `event` is read BEFORE the unwrap and kept: step 3.1 needs it to
537
+ // tell a write (append to the ledger) from a turn end (flush it), and the
538
+ // inner payload does not carry it.
539
+ let envelope_event = "";
540
+ const raw_native = payload["hook_event_name"];
541
+ if (typeof raw_native === "string") {
542
+ envelope_event = raw_native;
543
+ }
345
544
  if (["schema_version", "platform", "event", "payload"].every((k) => k in payload)) {
545
+ const raw_event = payload["event"];
546
+ envelope_event = typeof raw_event === "string" ? raw_event : envelope_event;
346
547
  const inner = payload["payload"];
347
548
  payload =
348
549
  typeof inner === "object" && inner !== null && !Array.isArray(inner)
@@ -350,6 +551,38 @@ export function run(
350
551
  : {};
351
552
  }
352
553
 
554
+ const replay = (process.env[REPLAY_ENV_VAR] ?? "").trim() === "1";
555
+
556
+ // Flush path (step 3.1): `stop` / `session_end` carry no roadmap path, so the
557
+ // roots come from the ledger the write path appended to. Nothing dirty → a
558
+ // read of one small file and exit, which is what makes this cheap enough to
559
+ // bind on a slot that already carries ten concerns.
560
+ if (FLUSH_EVENTS.has(envelope_event)) {
561
+ if (replay) {
562
+ if (verbose) {
563
+ process.stderr.write("roadmap-progress-hook: replay mode, skipping flush\n");
564
+ }
565
+ return 0;
566
+ }
567
+ const dirty = read_dirty_roots(consumer_root);
568
+ if (dirty.length === 0) {
569
+ return 0;
570
+ }
571
+ // Clear FIRST: a regenerator that hangs or throws must not leave a ledger
572
+ // that replays the same roots on every subsequent turn end. `_regenerate`
573
+ // already swallows its own failures, so a cleared ledger plus a failed
574
+ // regeneration degrades to "stale until the next roadmap write", which is
575
+ // the same worst case the debounce already accepts.
576
+ clear_dirty(consumer_root);
577
+ _regenerate(dirty, verbose);
578
+ if (verbose) {
579
+ process.stderr.write(
580
+ `roadmap-progress-hook: flushed ${dirty.length} dirty root(s) at ${envelope_event}\n`,
581
+ );
582
+ }
583
+ return 0;
584
+ }
585
+
353
586
  const tool = payload["tool_name"] || payload["toolName"] || payload["tool"];
354
587
  if (!(typeof tool === "string" && WRITE_TOOLS.has(tool))) {
355
588
  return 0;
@@ -369,59 +602,34 @@ export function run(
369
602
  return 0;
370
603
  }
371
604
 
372
- // Replay mode (`AGENT_CONFIG_REPLAY=1`) skips the regenerator subprocess
373
- // so fixture dispatches never rewrite agents/roadmaps-progress.md.
374
- if ((process.env[REPLAY_ENV_VAR] ?? "").trim() === "1") {
605
+ // Replay mode (`AGENT_CONFIG_REPLAY=1`) skips both the ledger write and the
606
+ // regenerator subprocess, so fixture dispatches leave no state and never
607
+ // rewrite agents/roadmaps-progress.md.
608
+ if (replay) {
375
609
  if (verbose) {
376
610
  process.stderr.write("roadmap-progress-hook: replay mode, skipping regenerator\n");
377
611
  }
378
612
  return 0;
379
613
  }
380
614
 
381
- for (const root of targetRoots) {
382
- const script = _resolve_regenerator(root);
383
- if (script === null) {
384
- // Phase 1 of road-to-hooks-actually-fire-in-consumers: log dispatch
385
- // issue directly (this hook runs as a subprocess from the universal
386
- // dispatcher; routing through the dispatcher would add latency for
387
- // no benefit).
388
- try {
389
- log_dispatch_issue(
390
- root,
391
- "roadmap-progress",
392
- "prerequisite_missing",
393
- "update_roadmap_progress.ts not found at any of: " +
394
- ".augment/scripts/, dist/agent-src/scripts/, " +
395
- "src/agent-src/scripts/",
396
- "./agent-config hooks:install --regen " + "(or ./agent-config init)",
397
- );
398
- } catch {
399
- // observability never breaks the hook
400
- }
401
- if (verbose) {
402
- process.stderr.write(
403
- `roadmap-progress-hook: regenerator not found for ${root}, skipping\n`,
404
- );
405
- }
406
- continue;
407
- }
408
-
409
- try {
410
- // The regenerator is a `.ts` script; run it through tsx (resolved from
411
- // a node_modules/.bin found by walking up from the regenerator's dir,
412
- // falling back to `npx tsx`), mirroring the dispatcher's `.ts` concerns.
413
- // `--repo-root` + cwd pin the regeneration to the edited file's own repo.
414
- const inv = _resolve_tsx_invocation(script);
415
- spawnSync(inv.command, [...inv.args, "--repo-root", root], {
416
- cwd: root,
417
- stdio: ["ignore", "ignore", "ignore"],
418
- timeout: 30000,
419
- env: hardenedSpawnEnv(),
420
- });
421
- } catch {
422
- // never propagate regenerator failures into the agent loop
615
+ // Write path (step 3.1): record and return. The spawn this used to pay per
616
+ // write now happens once, at turn end. A ledger that cannot be written falls
617
+ // back to regenerating inline — the debounce is an optimisation and must never
618
+ // be the reason a dashboard silently stops updating.
619
+ if (mark_dirty(consumer_root, Array.from(targetRoots))) {
620
+ if (verbose) {
621
+ process.stderr.write(
622
+ `roadmap-progress-hook: marked ${targetRoots.size} root(s) dirty for tool=${tool}\n`,
623
+ );
423
624
  }
625
+ return 0;
626
+ }
627
+ if (verbose) {
628
+ process.stderr.write(
629
+ "roadmap-progress-hook: ledger unwritable, regenerating inline\n",
630
+ );
424
631
  }
632
+ _regenerate(targetRoots, verbose);
425
633
 
426
634
  if (verbose) {
427
635
  process.stderr.write(`roadmap-progress-hook: regenerated for tool=${tool}\n`);