@event4u/agent-config 10.2.0 → 10.4.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 (96) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +168 -256
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/reference-repo.md +62 -0
  5. package/dist/agent-src/commands/analyze.md +2 -1
  6. package/dist/agent-src/commands/design-system/capture.md +44 -0
  7. package/dist/agent-src/commands/design-system/generate.md +53 -0
  8. package/dist/agent-src/commands/design-system/import.md +63 -0
  9. package/dist/agent-src/commands/design-system.md +66 -0
  10. package/dist/agent-src/commands/roadmap/create.md +4 -1
  11. package/dist/agent-src/personas/advisors/first-principles.md +6 -0
  12. package/dist/agent-src/rules/active-remediation.md +53 -2
  13. package/dist/agent-src/rules/code-provenance.md +66 -4
  14. package/dist/agent-src/rules/content-quoting-floor.md +6 -0
  15. package/dist/agent-src/rules/design-fidelity.md +49 -3
  16. package/dist/agent-src/skills/design-review/references/verification-automation.md +23 -1
  17. package/dist/agent-src/skills/design-system-capture/SKILL.md +9 -0
  18. package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +70 -0
  19. package/dist/agent-src/skills/fe-design/SKILL.md +27 -3
  20. package/dist/agent-src/skills/judge-synthesis/SKILL.md +30 -0
  21. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +11 -0
  22. package/dist/agent-src/skills/roadmap-writing/SKILL.md +8 -8
  23. package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
  24. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  25. package/dist/agent-src/templates/persona.md +26 -0
  26. package/dist/agent-src/templates/roadmaps.md +27 -7
  27. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  28. package/dist/agent-src/templates/skill.md +35 -1
  29. package/dist/cli/commands/commands.candidates.test.js +257 -0
  30. package/dist/cli/commands/commands.candidates.test.js.map +1 -0
  31. package/dist/cli/commands/commands.js +199 -0
  32. package/dist/cli/commands/commands.js.map +1 -1
  33. package/dist/cli/main.js +1 -0
  34. package/dist/cli/main.js.map +1 -1
  35. package/dist/cli/registry.js +1 -1
  36. package/dist/cli/registry.js.map +1 -1
  37. package/dist/cli-delegate/{chunk-E4RUZKXB.js → chunk-FV34EGEX.js} +1 -1
  38. package/dist/cli-delegate/chunk-RLN73PR5.js +157 -0
  39. package/dist/cli-delegate/{chunk-I5EAFRII.js → chunk-ZPSXJTLI.js} +2 -154
  40. package/dist/cli-delegate/cmd_conformance.js +672 -214
  41. package/dist/cli-delegate/cmd_doctor.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +8 -6
  43. package/dist/cli-delegate/cmd_session_recycle.js +5 -3
  44. package/dist/discovery/deprecation-report.md +1 -1
  45. package/dist/discovery/discovery-manifest.json +127 -22
  46. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  47. package/dist/discovery/discovery-manifest.summary.md +3 -3
  48. package/dist/discovery/dormancy-report.md +1 -1
  49. package/dist/discovery/orphan-report.md +1 -1
  50. package/dist/discovery/packs.json +9 -5
  51. package/dist/discovery/trust-report.md +2 -2
  52. package/dist/discovery/workspaces.json +8 -4
  53. package/dist/hooks/dispatch.js +1056 -411
  54. package/dist/mcp/registry-manifest.json +3 -3
  55. package/dist/mcp/server.json +2 -2
  56. package/dist/mcp/server.mjs +1103 -593
  57. package/dist/router.json +1 -1
  58. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +54 -0
  59. package/docs/guidelines/design-antipatterns.md +79 -10
  60. package/docs/guidelines/design-fidelity-mechanics.md +120 -2
  61. package/package.json +1 -1
  62. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +4 -1
  63. package/src/config/gate-coverage.yml +65 -10
  64. package/src/config/gate-violation-baselines.json +6 -0
  65. package/src/config/review-prompt-binding-baseline.json +19 -0
  66. package/src/config/rule-enforcement-baseline.json +0 -1
  67. package/src/scripts/_dispatch.bash +1 -0
  68. package/src/scripts/_lib/design_system_import.ts +987 -0
  69. package/src/scripts/ai_council/clients.ts +15 -1
  70. package/src/scripts/check_ci_local_parity.ts +60 -6
  71. package/src/scripts/check_review_prompt_binding.ts +553 -0
  72. package/src/scripts/check_suppression_hygiene.ts +19 -3
  73. package/src/scripts/design_slop_rules.test.ts +37 -6
  74. package/src/scripts/design_slop_rules.ts +150 -22
  75. package/src/scripts/design_system_import.ts +264 -0
  76. package/src/scripts/dispatch_r2_reviewer.ts +15 -1
  77. package/src/scripts/hook_manifest.yaml +71 -2
  78. package/src/scripts/hooks/concern_registry.ts +4 -0
  79. package/src/scripts/hooks/dispatch_hook.ts +10 -0
  80. package/src/scripts/hooks/host_semantics.ts +2 -0
  81. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +212 -0
  82. package/src/scripts/hooks/subagent_ledger_hook.ts +723 -0
  83. package/src/scripts/hooks/turn_end_gate_hook.ts +39 -2
  84. package/src/scripts/hooks/ui_route_nudge_hook.ts +55 -2
  85. package/src/scripts/lint_design_antipattern_parity.ts +201 -0
  86. package/src/scripts/lint_harvest_provenance.ts +483 -0
  87. package/src/scripts/lint_hook_manifest.ts +2 -0
  88. package/src/scripts/lint_rule_skill_pack_reach.ts +194 -13
  89. package/src/scripts/lint_skill_router_head.ts +254 -0
  90. package/src/scripts/report_consultation_rate.ts +126 -1
  91. package/src/scripts/run_skill_evals.ts +10 -5
  92. package/src/scripts/schemas/persona.schema.json +8 -0
  93. package/src/scripts/schemas/provenance-harvest.schema.json +39 -0
  94. package/src/scripts/sweep_dead_scan_roots.ts +51 -0
  95. package/tests/fixtures/hooks/subagent_start.json +7 -0
  96. package/tests/fixtures/hooks/subagent_stop.json +8 -0
@@ -48,6 +48,7 @@ import { fileURLToPath } from 'node:url';
48
48
 
49
49
  import { projectStoreSlug } from './_lib/cc_transcript.js';
50
50
  import {
51
+ isArtifactRead,
51
52
  isConsultation,
52
53
  isUiWrite,
53
54
  type ToolEvent,
@@ -81,6 +82,24 @@ export interface SessionMeasurement {
81
82
  consulted: number;
82
83
  /** Those turns followed later by opening the review skill. */
83
84
  reviewOpenedAfter: number;
85
+ /**
86
+ * Those turns that followed reading a provided design artifact.
87
+ *
88
+ * Shares this function's denominator on purpose — the two rates answer
89
+ * different questions over the SAME population ("did it consult the design
90
+ * surface" vs "did it read the handed-over source"), and measuring them on
91
+ * separate denominators is how two instruments about one behaviour stop
92
+ * being comparable.
93
+ */
94
+ artifactRead: number;
95
+ /** True when this session read a provided artifact at any point. */
96
+ sawArtifact: boolean;
97
+ /**
98
+ * True when this session's FIRST UI write came after an artifact read —
99
+ * the metric named "artifact read before first UI write". Meaningless
100
+ * unless `sawArtifact`, which is why it is reported over that denominator.
101
+ */
102
+ readBeforeFirstWrite: boolean;
84
103
  }
85
104
 
86
105
  export interface RateReport {
@@ -96,6 +115,16 @@ export interface RateReport {
96
115
  uiWriteTurns: number;
97
116
  consulted: number;
98
117
  reviewOpenedAfter: number;
118
+ /** UI-write turns preceded by reading a provided design artifact. */
119
+ artifactRead: number;
120
+ /**
121
+ * Sessions that read a provided artifact at all — the ONLY honest
122
+ * denominator for the read-before-write rate. A session with no handover
123
+ * cannot fail to read one.
124
+ */
125
+ handoverSessions: number;
126
+ /** Of those, the ones whose first UI write came after the read. */
127
+ handoverReadFirst: number;
99
128
  }
100
129
 
101
130
  /**
@@ -139,10 +168,20 @@ export function toolUseToEvent(part: Record<string, unknown>): ToolEvent | null
139
168
  * consult for the file it already wrote.
140
169
  */
141
170
  export function measureSession(timed: readonly TimedEvent[]): SessionMeasurement {
142
- const out: SessionMeasurement = { uiWriteTurns: 0, consulted: 0, reviewOpenedAfter: 0 };
171
+ const out: SessionMeasurement = {
172
+ uiWriteTurns: 0,
173
+ consulted: 0,
174
+ reviewOpenedAfter: 0,
175
+ artifactRead: 0,
176
+ sawArtifact: false,
177
+ readBeforeFirstWrite: false,
178
+ };
143
179
  let consulted = false;
180
+ let artifactRead = false;
144
181
  /** turn -> was the session already consulted when that turn first wrote UI */
145
182
  const writeTurns = new Map<number, boolean>();
183
+ /** turn -> had an artifact been read when that turn first wrote UI */
184
+ const artifactTurns = new Map<number, boolean>();
146
185
  let lastReviewOpenAt = -1;
147
186
  const reviewOpenPositions: number[] = [];
148
187
  const writePositions: Array<{ position: number; turn: number }> = [];
@@ -151,10 +190,18 @@ export function measureSession(timed: readonly TimedEvent[]): SessionMeasurement
151
190
  if (isUiWrite(event)) {
152
191
  if (!writeTurns.has(turn)) {
153
192
  writeTurns.set(turn, consulted);
193
+ artifactTurns.set(turn, artifactRead);
154
194
  writePositions.push({ position, turn });
155
195
  }
156
196
  return;
157
197
  }
198
+ // Evaluated INDEPENDENTLY of the consultation branch, never as an
199
+ // early return before it. The two predicates are not disjoint —
200
+ // `skills/design-review/references/design.html` satisfies both — so a
201
+ // `return` here would remove such an event from the consultation
202
+ // numerator and from the discharge-proxy latch. An earlier revision did
203
+ // exactly that while claiming in its comment to prevent it.
204
+ if (isArtifactRead(event)) artifactRead = true;
158
205
  if (isConsultation(event)) {
159
206
  consulted = true;
160
207
  if (event.file.replace(/\\/g, '/').toLowerCase().includes(`skills/${REVIEW_SURFACE}/`)) {
@@ -166,6 +213,20 @@ export function measureSession(timed: readonly TimedEvent[]): SessionMeasurement
166
213
 
167
214
  out.uiWriteTurns = writeTurns.size;
168
215
  for (const wasConsulted of writeTurns.values()) if (wasConsulted) out.consulted += 1;
216
+ for (const wasRead of artifactTurns.values()) if (wasRead) out.artifactRead += 1;
217
+ out.sawArtifact = artifactRead;
218
+ // KNOWN IMPRECISION, accepted: ordering inside ONE assistant message comes
219
+ // from the `tool_use` part index, and parallel tool calls in a single
220
+ // message carry no real order. A read and a write issued together can
221
+ // therefore score either way. Not repaired here — a transcript carries no
222
+ // per-call timestamp, so the fix is a different data source rather than a
223
+ // different loop, and the error is bounded to same-message pairs. It is a
224
+ // third reason the published rate is a ceiling.
225
+ //
226
+ // The FIRST write's outcome — `artifactTurns` is insertion-ordered on the
227
+ // first UI write of each turn, so its first value is that write's state.
228
+ const firstWrite = artifactTurns.values().next();
229
+ out.readBeforeFirstWrite = firstWrite.done !== true && firstWrite.value === true;
169
230
  // A single max comparison, not a scan per write: the proxy asks only whether
170
231
  // ANY review-open follows, so the last one decides every earlier write.
171
232
  void reviewOpenPositions;
@@ -216,6 +277,9 @@ export function measureStore(store: string, limit: number): RateReport {
216
277
  uiWriteTurns: 0,
217
278
  consulted: 0,
218
279
  reviewOpenedAfter: 0,
280
+ artifactRead: 0,
281
+ handoverSessions: 0,
282
+ handoverReadFirst: 0,
219
283
  };
220
284
  // A store that is not there and a store with nothing in it are different
221
285
  // findings, and only one of them is about the agent. Collapsing them into
@@ -255,6 +319,11 @@ export function measureStore(store: string, limit: number): RateReport {
255
319
  report.uiWriteTurns += measured.uiWriteTurns;
256
320
  report.consulted += measured.consulted;
257
321
  report.reviewOpenedAfter += measured.reviewOpenedAfter;
322
+ report.artifactRead += measured.artifactRead;
323
+ if (measured.sawArtifact) {
324
+ report.handoverSessions += 1;
325
+ if (measured.readBeforeFirstWrite) report.handoverReadFirst += 1;
326
+ }
258
327
  }
259
328
  return report;
260
329
  }
@@ -285,6 +354,62 @@ export function render(report: RateReport, threshold: number): string {
285
354
  lines.push(
286
355
  ` discharge PROXY (review opened) ${pct(report.reviewOpenedAfter, report.uiWriteTurns)} (${report.reviewOpenedAfter}/${report.uiWriteTurns})`,
287
356
  );
357
+ lines.push(
358
+ ` artifact read (all UI turns) ${pct(report.artifactRead, report.uiWriteTurns)} (${report.artifactRead}/${report.uiWriteTurns})`,
359
+ );
360
+ lines.push(
361
+ ` READ BEFORE FIRST WRITE ${pct(report.handoverReadFirst, report.handoverSessions)} (${report.handoverReadFirst}/${report.handoverSessions} handover session(s))`,
362
+ );
363
+ lines.push('');
364
+ lines.push(
365
+ ' READ BEFORE FIRST WRITE is the rate to quote. Its denominator is',
366
+ );
367
+ lines.push(
368
+ ' sessions that read a provided artifact at all — a session with no',
369
+ );
370
+ lines.push(
371
+ ' handover cannot fail to read one, so including it would dilute the',
372
+ );
373
+ lines.push(
374
+ ' rate toward the size of the estate rather than the behaviour.',
375
+ );
376
+ lines.push(
377
+ ' That denominator is narrower than it sounds, twice: "artifact" means',
378
+ );
379
+ lines.push(
380
+ ' a path ending design.html or under .claude/design-system/ — a handover',
381
+ );
382
+ lines.push(
383
+ ' under any other name is not counted — and a session that read one but',
384
+ );
385
+ lines.push(
386
+ ' wrote no UI is excluded, since it had nothing to be late for.',
387
+ );
388
+ lines.push(
389
+ ' The line above it shares the consultation denominator and is kept for',
390
+ );
391
+ lines.push(
392
+ ' comparability with that rate only.',
393
+ );
394
+ lines.push('');
395
+ lines.push(
396
+ ' A CEILING, NOT A MEASUREMENT, for two reasons. A session handed an',
397
+ );
398
+ lines.push(
399
+ ' artifact that NEVER reads it is invisible — nothing in a transcript',
400
+ );
401
+ lines.push(
402
+ ' marks a handover that was ignored, and that case IS the defect. And a',
403
+ );
404
+ lines.push(
405
+ ' search whose path argument happens to be the artifact counts as a read',
406
+ );
407
+ lines.push(
408
+ ' although nothing was read. Both errors point the same way: the true',
409
+ );
410
+ lines.push(
411
+ ' read-before-write rate is at most what is printed here.',
412
+ );
288
413
  lines.push('');
289
414
 
290
415
  if (report.sessionsWithUiWrite < threshold) {
@@ -10,10 +10,13 @@
10
10
  * JSON files (json.dumps indent=2 + trailing newline). No behaviour
11
11
  * changes.
12
12
  *
13
- * The `.agent-src.uncondensed/skills/` literal below is part of the
14
- * mirrored .py source — this is a faithful twin, so the legacy path is
15
- * replicated verbatim, not modernized (ADR-051 carve-out for faithful
16
- * twins).
13
+ * SKILLS_ROOT diverges from the mirrored .py source: it resolves the live
14
+ * skills tree through the shared resolver instead of replicating the retired
15
+ * container literal. Replicating it made every subcommand fail in
16
+ * `_skill_dir()` before doing any work — the twin was byte-faithful to a
17
+ * source that no longer exists and runnable against nothing. Documented per
18
+ * ADR-200 § 6 in `docs/migration/divergences/src-scripts-run_skill_evals.md`;
19
+ * an undocumented difference would be a regression by definition.
17
20
  *
18
21
  * Sub-agent SPAWNING is per-environment (Claude Code, Augment Code,
19
22
  * council) and is left as a stub `_spawn_subagent(...)` that authors
@@ -25,11 +28,13 @@ import * as fs from 'node:fs';
25
28
  import * as path from 'node:path';
26
29
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
30
 
31
+ import { SRC_SKILLS } from './_lib/agent_src.js';
32
+
28
33
  const _HERE = fileURLToPath(import.meta.url);
29
34
  // src/scripts/run_skill_evals.ts → parents[2] is the repo root (mirrors
30
35
  // `Path(__file__).resolve().parent.parent.parent` in the .py).
31
36
  export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
32
- export const SKILLS_ROOT = path.join(REPO_ROOT, '.agent-src.uncondensed', 'skills');
37
+ export const SKILLS_ROOT = SRC_SKILLS();
33
38
 
34
39
  /** JSON value type for eval specs / grades. */
35
40
  type JsonValue =
@@ -44,6 +44,14 @@
44
44
  "type": "string",
45
45
  "enum": ["package", "project"],
46
46
  "default": "package"
47
+ },
48
+ "sources": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string",
52
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
53
+ },
54
+ "description": "Optional harvest_id list (provenance/harvests.jsonl) this persona may cite as external doctrine. Three distinct states, and the difference is the point: ABSENT = unscoped, today's behaviour for every persona that does not opt in; [] = explicitly declares it asserts only from its own reasoning, nothing harvested; a non-empty list = scoped to exactly those ids. Every declared id must resolve to a ledger row — checked by lint_harvest_provenance.ts, which treats a persona entry as a citation exactly like an inline harvest marker. Not to be confused with the singular `source` field above, which records package-vs-project origin."
47
55
  }
48
56
  }
49
57
  }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "provenance-harvest.schema.json",
4
+ "title": "Provenance ledger harvest record",
5
+ "description": "One line of provenance/harvests.jsonl — a single knowledge claim adopted from an external source into this repo's own artefacts. Sibling of provenance-borrow.schema.json, which covers borrowed CODE; this one covers borrowed IDEAS. Contract: provenance/README.md. Enforced by src/scripts/lint_harvest_provenance.ts (hand-rolled validator, following the provenance-borrow precedent — this file documents the contract; it is not loaded at runtime).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["harvest_id", "stated_in", "source_ref", "evidence_locator", "harvested_at", "verdict"],
9
+ "properties": {
10
+ "harvest_id": {
11
+ "type": "string",
12
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
13
+ "description": "Kebab-case slug naming the harvested mechanism. This is the id an artefact cites; it must be unique across the ledger."
14
+ },
15
+ "stated_in": {
16
+ "type": "string",
17
+ "description": "Repo-relative path of the artefact asserting the claim, optionally suffixed ':<line>'. The path must exist."
18
+ },
19
+ "source_ref": {
20
+ "type": "string",
21
+ "description": "Either a public source pinned to a revision ('<url>@<sha>'), or an opaque token ('opaque:<id>' / 'ENC1:<payload>') when source-confidentiality keeps the source name out of the tracked tree. A bare URL with no revision is rejected — an unpinned reference cannot be re-verified."
22
+ },
23
+ "evidence_locator": {
24
+ "type": "string",
25
+ "minLength": 3,
26
+ "description": "Where inside the source the claim was read — a file:line, a section anchor, or an equivalent locator. Free-form because sources differ, but never empty: a claim whose evidence cannot be located again is not a citation."
27
+ },
28
+ "harvested_at": {
29
+ "type": "string",
30
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
31
+ "description": "ISO-8601 date (YYYY-MM-DD) the harvest landed."
32
+ },
33
+ "verdict": {
34
+ "type": "string",
35
+ "enum": ["adopt", "adapt"],
36
+ "description": "How the mechanism entered this repo. Only adopt and adapt produce a row — a rejected, already-present, or unclear finding is recorded in the analysis document, never in the ledger, because nothing in the tree cites it."
37
+ }
38
+ }
39
+ }
@@ -148,6 +148,36 @@ export function classify(rel: string): TriageClass {
148
148
  return 'C';
149
149
  }
150
150
 
151
+ /**
152
+ * Non-gate scripts eligible for the Class-A advisory pass.
153
+ *
154
+ * WHY A SECOND POPULATION EXISTS AT ALL. The gate population above is
155
+ * deliberately shared with the ratchet and the registration test, because three
156
+ * sites once carried their own regex and disagreed. That sharing is worth
157
+ * keeping — but it means the sweep's reach is a FILENAME PREFIX, and a script
158
+ * reading a retired container is dead whether or not its name starts with one
159
+ * of five words. Measured: `run_skill_evals.ts` roots its `SKILLS_ROOT` at the
160
+ * skills subtree of the container ADR-051 retired (see `CLASS_A_RETIRED`), and
161
+ * every subcommand fails in `_skill_dir()` before doing any work — while its
162
+ * sibling `skill_trigger_eval.ts`, same retired root and a `skill_` prefix, is
163
+ * reported. One visible, one not, on the prefix alone.
164
+ *
165
+ * So this pass widens the READING, never the shared population: it is
166
+ * Class-A-only (a closed set of retired containers, needing no allowlist to stay
167
+ * honest), it is advisory, and it feeds neither the ratchet count nor the exit
168
+ * code. Widening `matchesGatePattern` instead would move the ratchet base for a
169
+ * reason unrelated to any gate.
170
+ */
171
+ export function isNonGateScript(filename: string): boolean {
172
+ if (!filename.endsWith('.ts')) return false;
173
+ if (filename.endsWith('.d.ts') || filename.endsWith('.test.ts')) return false;
174
+ // This file declares the retired containers as a classification table
175
+ // (`CLASS_A_RETIRED`); reading its own constant back as a dead root is a
176
+ // self-report, not a finding.
177
+ if (filename === 'sweep_dead_scan_roots.ts') return false;
178
+ return !isGateFile(filename);
179
+ }
180
+
151
181
  export interface Finding {
152
182
  rel: string;
153
183
  names: string;
@@ -827,6 +857,20 @@ export function main(argv: string[]): number {
827
857
  if (r.confirmed.length || r.unproven.length) results.set(f, r);
828
858
  }
829
859
 
860
+ // Class-A advisory over the scripts the shared gate population cannot reach.
861
+ // Reuses the same extractor, so a prose mention or a report string is not a
862
+ // finding — only a literal join over a recognised root base is.
863
+ const advisory: string[] = [];
864
+ const advisoryPop = fs.existsSync(args.scripts)
865
+ ? fs.readdirSync(args.scripts).filter(isNonGateScript).sort()
866
+ : [];
867
+ for (const f of advisoryPop) {
868
+ const r = analyze(fs.readFileSync(path.join(args.scripts, f), 'utf-8'), repoRoot);
869
+ for (const h of [...r.confirmed, ...r.unproven]) {
870
+ if (classify(h.rel) === 'A') advisory.push(` advisory [A] ${f}: ${h.rel} (${h.names})`);
871
+ }
872
+ }
873
+
830
874
  const used = new Set<Disposition>();
831
875
  const disposed = (script: string, rel: string): Disposition | undefined => {
832
876
  const hit = ledger.find((l) => l.script === script && l.rel === rel);
@@ -899,6 +943,13 @@ export function main(argv: string[]): number {
899
943
  if (!args.quiet) {
900
944
  process.stdout.write(`sweep_dead_scan_roots: ${pop.length} gate script(s) scanned\n`);
901
945
  for (const l of lines) process.stdout.write(`${l}\n`);
946
+ if (advisory.length) {
947
+ process.stdout.write(
948
+ `sweep_dead_scan_roots: ${advisoryPop.length} non-gate script(s) scanned ` +
949
+ '(Class-A advisory — not counted, not gated)\n',
950
+ );
951
+ for (const l of advisory) process.stdout.write(`${l}\n`);
952
+ }
902
953
  }
903
954
 
904
955
  if (stale.length) {
@@ -0,0 +1,7 @@
1
+ {
2
+ "session_id": "fixture-subagent-start-001",
3
+ "agent_id": "fixture-agent-0001",
4
+ "agent_type": "Explore",
5
+ "cwd": "/workspace",
6
+ "platform_hint": "claude"
7
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "session_id": "fixture-subagent-stop-001",
3
+ "agent_id": "fixture-agent-0001",
4
+ "agent_type": "Explore",
5
+ "last_assistant_message": "{\"summary\":\"fixture envelope\",\"handoff\":\"none\",\"confidence\":\"high\",\"findings\":[],\"risks\":[]}",
6
+ "cwd": "/workspace",
7
+ "platform_hint": "claude"
8
+ }