@event4u/agent-config 14.4.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.
@@ -51,6 +51,12 @@
51
51
  * checkbox delta) → allow (event: halt-stall)
52
52
  * otherwise → BLOCK + continue (event: engage)
53
53
  *
54
+ * Every event this concern writes carries `workspace_root`, `git_dir`,
55
+ * `git_common_dir` and `claim_path` — the two-tree provenance, per
56
+ * `provenance()` below. Without them the ledger records that an engagement
57
+ * happened but not that it crossed the tree boundary the fix was about, and
58
+ * crossing it is the whole claim.
59
+ *
54
60
  * Stall counts CONSECUTIVE ENGAGEMENTS whose open-step count did not move.
55
61
  * The upstream stalls on 5-of-5 completed-count readings; AC's checkbox
56
62
  * signal is finer-grained (a step in progress usually flips SOMETHING), so
@@ -88,7 +94,8 @@ import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
88
94
  import { readHookStdin } from './hook_stdin.js';
89
95
  import { is_replay_mode } from './state_io.js';
90
96
  import { readTranscriptTail } from './turn_end_gate_hook.js';
91
- import { read_claimed_slug } from '../session_register_hook.js';
97
+ import { resolve_claim } from '../session_register_hook.js';
98
+ import { git_common_dir, git_dir } from '../_lib/git_common_dir.js';
92
99
  import {
93
100
  deriveSessionKey,
94
101
  parseRecord,
@@ -386,6 +393,48 @@ function readState(file: string): RunState | null {
386
393
  }
387
394
  }
388
395
 
396
+ /**
397
+ * The four fields that make an event's two-tree property checkable by a third
398
+ * party holding nothing but the ledger line.
399
+ *
400
+ * The defect this concern shipped with was a writer and a reader resolving
401
+ * DIFFERENT trees: `sessions:claim` wrote under `process.cwd()` (the operator's
402
+ * worktree) while this concern read under `--project-dir` (the parent
403
+ * checkout), so the claim was never found and no event was written. An empty
404
+ * ledger looked exactly like a healthy idle run, which is how it survived a
405
+ * release.
406
+ *
407
+ * A reader therefore has to see, from the line alone, which trees were in play
408
+ * and where the contract was actually read out of. A boolean such as
409
+ * `worktree_started: true` would be another assertion by the system under
410
+ * observation rather than a check on it; the falsifiable facts are concrete
411
+ * (AI council 2026-08-19, 2/2 convergent):
412
+ *
413
+ * `git_dir !== git_common_dir` → this IS a linked worktree
414
+ * `claim_path` under `git_common_dir` → the contract crossed into the shared
415
+ * root, which is the fix working
416
+ *
417
+ * `process.cwd()` is deliberately NOT among them. The dispatcher chdirs into
418
+ * `--project-dir` before the chain runs, so at concern time cwd equals
419
+ * `workspace_root` on every shipped path — a field that can never disagree with
420
+ * another cannot falsify anything, and shipping it as provenance would be the
421
+ * decorative-evidence shape this whole change exists to remove. Measured while
422
+ * writing the test that asserted the opposite.
423
+ *
424
+ * Absolute paths, deliberately: a relative form is interpreted against whichever
425
+ * root the reader happens to hold, which is the exact ambiguity being removed.
426
+ * These are local filesystem paths in gitignored runtime state, never published
427
+ * — the same posture the session register already takes.
428
+ */
429
+ export function provenance(workspaceRoot: string, claimPath: string): JsonObject {
430
+ return {
431
+ workspace_root: workspaceRoot,
432
+ git_dir: git_dir(workspaceRoot) ?? '',
433
+ git_common_dir: git_common_dir(workspaceRoot) ?? '',
434
+ claim_path: claimPath,
435
+ };
436
+ }
437
+
389
438
  function appendEvent(workspaceRoot: string, record: JsonObject): void {
390
439
  try {
391
440
  const file = path.join(workspaceRoot, EVENTS_RELPATH);
@@ -438,8 +487,12 @@ export function main(): number {
438
487
  str(envelope['session_id'] as JsonValue | undefined);
439
488
 
440
489
  // ── contract gate ────────────────────────────────────────────────
441
- const slug = read_claimed_slug(workspaceRoot, sessionId || null);
442
- if (slug === null) return EXIT_ALLOW;
490
+ const claim = resolve_claim(workspaceRoot, sessionId || null);
491
+ if (claim === null) return EXIT_ALLOW;
492
+ const slug = claim.slug;
493
+ // The two-root provenance, carried on every event this run emits. See
494
+ // `provenance()` for why these three and not a boolean.
495
+ const roots = provenance(workspaceRoot, claim.path);
443
496
 
444
497
  const roadmapPath = path.join(workspaceRoot, 'agents', 'roadmaps', `${slug}.md`);
445
498
  let roadmapText: string;
@@ -491,6 +544,7 @@ export function main(): number {
491
544
  roadmap: slug,
492
545
  turn: turnOrdinal,
493
546
  at: new Date().toISOString(),
547
+ ...roots,
494
548
  });
495
549
  return EXIT_ALLOW;
496
550
  }
@@ -529,6 +583,7 @@ export function main(): number {
529
583
  open: scan.open,
530
584
  blocked: scan.blocked,
531
585
  at: new Date().toISOString(),
586
+ ...roots,
532
587
  });
533
588
  if (action === 'complete') {
534
589
  // Only a COMPLETION clears the state, so a later run on the same
@@ -580,6 +635,7 @@ export function main(): number {
580
635
  open: scan.open,
581
636
  blocked: scan.blocked,
582
637
  at: new Date().toISOString(),
638
+ ...roots,
583
639
  });
584
640
 
585
641
  process.stderr.write(_continuationText(slug, scan, state.iterations));
@@ -237,7 +237,23 @@ function _read_claim_file(file: string): RoadmapClaim | null {
237
237
  }
238
238
 
239
239
  /**
240
- * The roadmap slug THIS session has claimed, or `null`. Never throws.
240
+ * A resolved claim plus the file it was actually read from.
241
+ *
242
+ * The path is not decoration. The defect this whole surface exists to prevent
243
+ * was a WRITER and a READER resolving different trees, and the only fact that
244
+ * falsifies it is the concrete pair of roots — a boolean asserting "worktree
245
+ * started" is another claim by the system under observation, not a check on it
246
+ * (AI council 2026-08-19, 2/2, on the run-continuation observation).
247
+ */
248
+ export interface ResolvedClaim {
249
+ slug: string;
250
+ /** Absolute path of the claim file this read came from. */
251
+ path: string;
252
+ }
253
+
254
+ /**
255
+ * The roadmap slug THIS session has claimed, and the file it came from — or
256
+ * `null`. Never throws.
241
257
  *
242
258
  * Two reads, in order, and the second one is why a peer's claim can no longer be
243
259
  * inherited: the per-session file first, then the legacy per-worktree file —
@@ -246,14 +262,14 @@ function _read_claim_file(file: string): RoadmapClaim | null {
246
262
  * that session's claim, and reading it as this session's is exactly the defect
247
263
  * that put one archived slug on four live records.
248
264
  */
249
- export function read_claimed_slug(
265
+ export function resolve_claim(
250
266
  workspace_root: string,
251
267
  // REQUIRED, on the reviewer's point: an optional id means a caller that forgets
252
268
  // it silently gets legacy semantics instead of a type error, and there is
253
269
  // exactly one call site that matters (`build_record`). `null` stays a legal
254
270
  // value — it is the host-exports-nothing case — but it has to be passed.
255
271
  session_id: string | null,
256
- ): string | null {
272
+ ): ResolvedClaim | null {
257
273
  const own = roadmap_claim_rel(session_id);
258
274
  if (own !== ROADMAP_CLAIM_REL) {
259
275
  // Per-session, shared dir first, then the pre-fix per-tree path. The
@@ -262,16 +278,20 @@ export function read_claimed_slug(
262
278
  for (const p of claim_read_paths(workspace_root, session_id)) {
263
279
  if (path.basename(p) === path.basename(ROADMAP_CLAIM_REL)) continue;
264
280
  const mine = _read_claim_file(p);
265
- if (mine !== null) return mine.slug;
281
+ if (mine !== null) return { slug: mine.slug, path: p };
266
282
  }
267
283
  }
268
284
  // The legacy per-WORKTREE file, in the same two locations. Unchanged in
269
285
  // meaning: it is a claim on the checkout, not on a session.
270
286
  let legacy: RoadmapClaim | null = null;
287
+ let legacy_path = '';
271
288
  for (const p of claim_read_paths(workspace_root, session_id)) {
272
289
  if (path.basename(p) !== path.basename(ROADMAP_CLAIM_REL)) continue;
273
290
  legacy = _read_claim_file(p);
274
- if (legacy !== null) break;
291
+ if (legacy !== null) {
292
+ legacy_path = p;
293
+ break;
294
+ }
275
295
  }
276
296
  if (legacy === null) {
277
297
  return null;
@@ -293,7 +313,21 @@ export function read_claimed_slug(
293
313
  if (id !== '') {
294
314
  return null;
295
315
  }
296
- return legacy.slug;
316
+ return { slug: legacy.slug, path: legacy_path };
317
+ }
318
+
319
+ /**
320
+ * The roadmap slug THIS session has claimed, or `null`. Never throws.
321
+ *
322
+ * Thin projection of `resolve_claim` — kept because every existing caller wants
323
+ * the slug alone, and widening their return type to carry a path none of them
324
+ * reads would be a signature change for no consumer.
325
+ */
326
+ export function read_claimed_slug(
327
+ workspace_root: string,
328
+ session_id: string | null,
329
+ ): string | null {
330
+ return resolve_claim(workspace_root, session_id)?.slug ?? null;
297
331
  }
298
332
 
299
333
  /**