@event4u/agent-config 14.4.0 → 14.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 (48) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +132 -291
  3. package/dist/agent-src/commands/analyze/decision.md +7 -0
  4. package/dist/agent-src/commands/optimize/project.md +10 -0
  5. package/dist/agent-src/commands/roadmap/process-full.md +45 -3
  6. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +130 -1
  7. package/dist/agent-src/rules/decision-revisit-gate.md +108 -7
  8. package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
  9. package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
  10. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  11. package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-P6LQDIWK.js} +15 -1
  12. package/dist/cli-delegate/cmd_conformance.js +1 -1
  13. package/dist/cli-delegate/cmd_handoff.js +1 -1
  14. package/dist/cli-delegate/cmd_session_recycle.js +1 -1
  15. package/dist/discovery/deprecation-report.md +1 -1
  16. package/dist/discovery/discovery-manifest.json +8 -8
  17. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  18. package/dist/discovery/discovery-manifest.summary.md +1 -1
  19. package/dist/discovery/dormancy-report.md +1 -1
  20. package/dist/discovery/orphan-report.md +1 -1
  21. package/dist/discovery/packs.json +2 -2
  22. package/dist/discovery/trust-report.md +1 -1
  23. package/dist/discovery/workspaces.json +2 -2
  24. package/dist/hooks/dispatch.js +419 -94
  25. package/dist/mcp/registry-manifest.json +2 -2
  26. package/dist/mcp/server.json +2 -2
  27. package/dist/router.json +1 -1
  28. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
  29. package/package.json +1 -1
  30. package/src/config/estate-count-budget.json +36 -1
  31. package/src/config/pack-size-budget.json +3 -2
  32. package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
  33. package/src/scripts/_lib/scope_guard.sh +62 -3
  34. package/src/scripts/_lib/subagent_capsule.ts +81 -1
  35. package/src/scripts/adr/regenerate_index.ts +13 -2
  36. package/src/scripts/adr_cite_check.ts +424 -0
  37. package/src/scripts/bench_hook_injection.ts +18 -1
  38. package/src/scripts/check_adr_frontmatter.ts +187 -1
  39. package/src/scripts/check_single_delivery.ts +473 -0
  40. package/src/scripts/hooks/host_semantics.ts +28 -0
  41. package/src/scripts/hooks/injection_budget.ts +152 -16
  42. package/src/scripts/hooks/run_continuation_hook.ts +918 -46
  43. package/src/scripts/install.sh +12 -2
  44. package/src/scripts/memory_consolidation.ts +189 -0
  45. package/src/scripts/memory_eviction.ts +384 -0
  46. package/src/scripts/report_layer_overlap.ts +72 -0
  47. package/src/scripts/report_memory_pointers.ts +622 -0
  48. package/src/scripts/session_register_hook.ts +144 -21
@@ -17,10 +17,27 @@
17
17
  * the fix is a registered decision, not a silent default bump
18
18
  * - per-slot SUM over all bound concerns > the slot cap → RED
19
19
  *
20
+ * NOT FULLY DETERMINISTIC — this paragraph replaces a claim that said it was.
21
+ * The fixture path was described as "deterministic, CI-reproducible". For most
22
+ * concerns it is; they read only the committed envelope. But `end-review-nudge`
23
+ * decides whether to fire from the LIVE working-tree diff plus a once-per-session
24
+ * latch, so one commit reads 1,140 B with uncommitted changes present and 922 B
25
+ * without them. Two values, observed three times in one session as the tree went
26
+ * dirty, clean and dirty again — the earlier wording here said "three different
27
+ * values", which counted the readings and not the values, and an R2 review was
28
+ * right to call that an unbacked number inside the paragraph that exists to
29
+ * replace an unbacked claim.
30
+ * Two consequences. A single run is a reading of THIS checkout right now, so its
31
+ * number must never be pinned as acceptance evidence (a roadmap note tried, and
32
+ * had to be corrected). And the BREACH verdict is unaffected regardless, because
33
+ * a state-dependent concern can only push the sum UP: a green run is still a
34
+ * green run. Documented rather than fixed, because making this hermetic means
35
+ * changing what the concern reads — a change to the concern, not to its bench.
36
+ *
20
37
  * HONEST SCOPE, stated up front: concerns are conditional-silence by design —
21
38
  * a generic fixture triggers few of them, so most measure 0 bytes here. The
22
39
  * gate therefore enforces the cap on whatever DOES fire under the committed
23
- * fixtures (deterministic, CI-reproducible), while the census mode
40
+ * fixtures, while the census mode
24
41
  * (`--record`) measures REAL sessions: it appends counts-only lines
25
42
  * ({ts, slot, concern, bytes}) to agents/runtime/state/injection-census.jsonl
26
43
  * — the repeat-injection-census blocker's instrument. Claiming the fixture
@@ -51,6 +51,92 @@ export const REVIEW_TRIGGER_SINCE = '2026-07-25';
51
51
  /** Fields every ADR must carry — the shape that already exists in practice. */
52
52
  const REQUIRED = ['adr', 'status', 'date', 'decision'] as const;
53
53
 
54
+ /**
55
+ * Reopen-authority vocabulary (`adr-layout.md` § Reopen authority).
56
+ *
57
+ * Both fields are OPTIONAL and stay optional. An absent `reopen_policy`
58
+ * resolves to `unclassified` at the reader, never to `owner`: with 146 accepted
59
+ * ADRs a fail-closed default would encode the existing blockage into the new
60
+ * schema, which is the one thing both council seats (2026-08-19) rejected in
61
+ * the same words. This validator therefore checks the VALUE when present and
62
+ * never requires the KEY.
63
+ */
64
+ const ALLOWED_REOPEN_POLICY = new Set(['directional', 'owner', 'unclassified']);
65
+
66
+ const ALLOWED_PROTECTED_DIMENSIONS = new Set([
67
+ 'purpose',
68
+ 'security_floor',
69
+ 'privacy_floor',
70
+ 'external_commitment',
71
+ 'governance',
72
+ 'none',
73
+ ]);
74
+
75
+ /** Split an inline `[a, b]` list or a folded multi-value string into members. */
76
+ export function split_dimensions(raw: string): string[] {
77
+ return raw
78
+ .replace(/^\[/, '')
79
+ .replace(/\]$/, '')
80
+ .split(/[,\s]+/)
81
+ .map((s) =>
82
+ s
83
+ .trim()
84
+ .replace(/^-\s*/, '')
85
+ .replace(/^["'](.*)["']$/, '$1'),
86
+ )
87
+ .filter((s) => s !== '');
88
+ }
89
+
90
+ /**
91
+ * Validate the two optional reopen-authority fields — value only, never
92
+ * presence. A wrong enum is a real error (it silently mis-routes authority);
93
+ * an absent field is the documented default and not a finding.
94
+ */
95
+ export function check_reopen_authority(
96
+ rel: string,
97
+ fm: Record<string, string>,
98
+ findings: AdrFinding[],
99
+ ): void {
100
+ const policy = fm['reopen_policy'];
101
+ if (policy !== undefined && policy !== '' && !ALLOWED_REOPEN_POLICY.has(policy)) {
102
+ findings.push({
103
+ file: rel,
104
+ level: 'error',
105
+ message:
106
+ `\`reopen_policy\` \`${policy}\` is not one of: ` +
107
+ `${[...ALLOWED_REOPEN_POLICY].sort().join(', ')} (absent = unclassified)`,
108
+ });
109
+ }
110
+
111
+ const dims = fm['protected_dimensions'];
112
+ if (dims !== undefined && dims !== '') {
113
+ for (const d of split_dimensions(dims)) {
114
+ if (!ALLOWED_PROTECTED_DIMENSIONS.has(d)) {
115
+ findings.push({
116
+ file: rel,
117
+ level: 'error',
118
+ message:
119
+ `\`protected_dimensions\` member \`${d}\` is not one of: ` +
120
+ `${[...ALLOWED_PROTECTED_DIMENSIONS].sort().join(', ')}`,
121
+ });
122
+ }
123
+ }
124
+ }
125
+
126
+ // `owner` without a named dimension is the frozen-over-classification shape
127
+ // the contract warns about: it reserves every future transition, including
128
+ // the ones that only strengthen the decision, and records no reason.
129
+ if (policy === 'owner' && (dims === undefined || dims === '')) {
130
+ findings.push({
131
+ file: rel,
132
+ level: 'warn',
133
+ message:
134
+ '`reopen_policy: owner` without `protected_dimensions` — name the reserved ' +
135
+ 'interest, or the reservation cannot be checked against a proposed transition',
136
+ });
137
+ }
138
+ }
139
+
54
140
  const ALLOWED_STATUS = new Set([
55
141
  'accepted',
56
142
  'proposed',
@@ -141,6 +227,98 @@ export function check_one(rel: string, text: string): AdrFinding[] {
141
227
  message: `\`review_trigger\` is a cadence, not a condition: "${trigger}". A calendar review is ignored; an event fires.`,
142
228
  });
143
229
  }
230
+ check_reopen_authority(rel, fm, findings);
231
+ check_amendment_shape(rel, fm, findings);
232
+ return findings;
233
+ }
234
+
235
+ /** `ADR-035`, `035`, `35` → `35`. Returns null when nothing resolves. */
236
+ export function adr_number(raw: string): string | null {
237
+ const m = /(\d{1,4})/.exec(raw.trim());
238
+ return m?.[1] === undefined ? null : String(Number(m[1]));
239
+ }
240
+
241
+ /**
242
+ * Validate `amends:` / `amended_by:` shape. The reciprocal half is checked in
243
+ * `check_amendment_links` (it needs the whole corpus); this one only rejects a
244
+ * value that is neither the em-dash placeholder nor an ADR reference, so a
245
+ * typo'd link is caught before it becomes an invisible one-sided amendment.
246
+ */
247
+ export function check_amendment_shape(
248
+ rel: string,
249
+ fm: Record<string, string>,
250
+ findings: AdrFinding[],
251
+ ): void {
252
+ for (const key of ['amends', 'amended_by'] as const) {
253
+ const v = fm[key];
254
+ if (v === undefined || v === '' || v === '—') continue;
255
+ for (const part of v.split(',')) {
256
+ if (adr_number(part) === null) {
257
+ findings.push({
258
+ file: rel,
259
+ level: 'error',
260
+ message: `\`${key}\` value \`${part.trim()}\` names no ADR number`,
261
+ });
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Corpus-level check: every `amends:` has its reciprocal `amended_by:` and vice
269
+ * versa. This is the half the `supersedes:` / `superseded_by:` pair never had —
270
+ * and its absence is exactly how ADR-035 kept asserting a rejection ADR-232 had
271
+ * reopened. A one-sided link is invisible from the side that is stale, which is
272
+ * the side a reader lands on first.
273
+ */
274
+ export function check_amendment_links(
275
+ files: { rel: string; fm: Record<string, string> }[],
276
+ ): AdrFinding[] {
277
+ const findings: AdrFinding[] = [];
278
+ const byNumber = new Map<string, { rel: string; fm: Record<string, string> }>();
279
+ for (const f of files) {
280
+ const n = f.fm['adr'] === undefined ? null : adr_number(f.fm['adr']);
281
+ if (n !== null) byNumber.set(n, f);
282
+ }
283
+
284
+ const reciprocal = { amends: 'amended_by', amended_by: 'amends' } as const;
285
+ for (const f of files) {
286
+ for (const key of ['amends', 'amended_by'] as const) {
287
+ const v = f.fm[key];
288
+ if (v === undefined || v === '' || v === '—') continue;
289
+ const self = f.fm['adr'] === undefined ? null : adr_number(f.fm['adr']);
290
+ for (const part of v.split(',')) {
291
+ const target = adr_number(part);
292
+ if (target === null) continue;
293
+ const other = byNumber.get(target);
294
+ if (other === undefined) {
295
+ findings.push({
296
+ file: f.rel,
297
+ level: 'error',
298
+ message: `\`${key}: ${part.trim()}\` points at an ADR that does not exist`,
299
+ });
300
+ continue;
301
+ }
302
+ const back = other.fm[reciprocal[key]] ?? '';
303
+ const names_self =
304
+ self !== null &&
305
+ back
306
+ .split(',')
307
+ .map((s) => adr_number(s))
308
+ .includes(self);
309
+ if (!names_self) {
310
+ findings.push({
311
+ file: f.rel,
312
+ level: 'error',
313
+ message:
314
+ `\`${key}: ${part.trim()}\` is one-sided — ${other.rel} carries no ` +
315
+ `reciprocal \`${reciprocal[key]}\`. A one-sided amendment link is ` +
316
+ `invisible from the stale side.`,
317
+ });
318
+ }
319
+ }
320
+ }
321
+ }
144
322
  return findings;
145
323
  }
146
324
 
@@ -163,13 +341,20 @@ export function check(dir: string = ADR_DIR, ledger?: GateLedger): AdrFinding[]
163
341
  const candidates = fs.readdirSync(dir).filter((n) => n.endsWith('.md')).sort();
164
342
  ledger?.plan(candidates);
165
343
 
344
+ // Collected for the corpus-level reciprocal-amendment check below, which
345
+ // cannot be decided from one file.
346
+ const corpus: { rel: string; fm: Record<string, string> }[] = [];
347
+
166
348
  for (const name of candidates) {
167
349
  if (!/^ADR-.*\.md$/.test(name)) {
168
350
  ledger?.outOfScope(name, 'not_applicable_kind');
169
351
  continue;
170
352
  }
171
353
  const rel = path.relative(REPO_ROOT, path.join(dir, name));
172
- const own = check_one(rel, fs.readFileSync(path.join(dir, name), 'utf-8'));
354
+ const text = fs.readFileSync(path.join(dir, name), 'utf-8');
355
+ const parsed = parse_frontmatter(text);
356
+ if (parsed !== null) corpus.push({ rel, fm: parsed });
357
+ const own = check_one(rel, text);
173
358
  findings.push(...own);
174
359
 
175
360
  const errors = own.filter((f) => f.level === 'error').length;
@@ -179,6 +364,7 @@ export function check(dir: string = ADR_DIR, ledger?: GateLedger): AdrFinding[]
179
364
  ledger?.complete(name);
180
365
  }
181
366
  }
367
+ findings.push(...check_amendment_links(corpus));
182
368
  return findings;
183
369
  }
184
370