@kontourai/flow-agents 3.1.0 → 3.2.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.
- package/.github/workflows/ci.yml +4 -0
- package/CHANGELOG.md +17 -0
- package/build/src/cli/assignment-provider.d.ts +45 -0
- package/build/src/cli/assignment-provider.js +97 -12
- package/build/src/cli/workflow-sidecar.d.ts +14 -4
- package/build/src/cli/workflow-sidecar.js +100 -10
- package/context/contracts/assignment-provider-contract.md +1 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/scripts/hooks/config-protection.js +11 -4
- package/context/scripts/hooks/stop-goal-fit.js +259 -4
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +111 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_checkpoint_signing.sh +4 -3
- package/evals/integration/test_gate_lockdown.sh +36 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +14 -6
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
- package/evals/run.sh +6 -0
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/deliver/SKILL.md +52 -11
- package/kits/builder/skills/design-probe/SKILL.md +10 -0
- package/kits/builder/skills/execute-plan/SKILL.md +13 -0
- package/kits/builder/skills/fix-bug/SKILL.md +17 -0
- package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
- package/kits/builder/skills/plan-work/SKILL.md +9 -0
- package/kits/builder/skills/pull-work/SKILL.md +10 -0
- package/kits/builder/skills/review-work/SKILL.md +11 -0
- package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
- package/kits/builder/skills/verify-work/SKILL.md +11 -0
- package/kits/knowledge/adapters/default-store/index.js +56 -15
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +29 -11
- package/kits/knowledge/adapters/shared/codec.js +124 -0
- package/kits/knowledge/docs/store-contract.md +405 -3
- package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
- package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
- package/kits/knowledge/evals/contract-suite/suite.test.js +36 -0
- package/kits/knowledge/evals/freshness/suite.test.js +339 -0
- package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1 -1
- package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
- package/package.json +1 -1
- package/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +144 -26
- package/scripts/hooks/config-protection.js +11 -4
- package/scripts/hooks/stop-goal-fit.js +259 -4
- package/src/cli/assignment-provider.ts +110 -12
- package/src/cli/workflow-sidecar.ts +99 -10
|
@@ -559,31 +559,129 @@ function claimLocalFile(argv: string[]): number {
|
|
|
559
559
|
return 0;
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Wave 1 (#292) extraction: the durable-write body previously inlined inside releaseLocalFile's
|
|
564
|
+
* withSubjectLock() closure, now a parameter-driven pure function so the Stop hook's non-terminal
|
|
565
|
+
* release lifecycle (scripts/hooks/stop-goal-fit.js, #292 Wave 2) can reuse the EXACT same release
|
|
566
|
+
* logic — actor-ownership verification, audit-trail append, atomic write under withSubjectLock —
|
|
567
|
+
* rather than reimplementing a second, parallel release path. releaseLocalFile (CLI wrapper,
|
|
568
|
+
* below) is now a thin parse-args/print-envelope shell around this, mirroring the
|
|
569
|
+
* performLocalSupersede/supersedeLocalFile extraction shape exactly.
|
|
570
|
+
*
|
|
571
|
+
* Two behaviors are deliberately DIFFERENT from a naive inline release, both required for the
|
|
572
|
+
* Stop hook's idempotent, actor-scoped lifecycle release (never for the interactive CLI, which
|
|
573
|
+
* keeps `tolerateNoActiveClaim` unset/false and therefore 100% of its prior throw-on-error shape):
|
|
574
|
+
*
|
|
575
|
+
* - `opts.tolerateNoActiveClaim === true` and there is no existing record, or the existing
|
|
576
|
+
* record's status is not `"claimed"`: return `null` (a tolerated no-op) instead of throwing
|
|
577
|
+
* "no active claim to release". This is the one deliberate idempotency change vs today's
|
|
578
|
+
* releaseLocalFile — a second release call (e.g. a double Stop event) must be a safe no-op.
|
|
579
|
+
* - `releasedBy` is provided and does not match the existing record's holder: never force-release
|
|
580
|
+
* a claim held by a different actor — return `null` (if tolerateNoActiveClaim) or throw
|
|
581
|
+
* (otherwise), same as the no-active-claim case. The comparison mirrors computeEffectiveState()'s
|
|
582
|
+
* `record.actor_key || helper.serializeActor(record.actor)` canonical-key preference EXACTLY
|
|
583
|
+
* (actor_key-first, falling back to serializeActor only when actor_key is absent) — the read
|
|
584
|
+
* path (status/effective-state) and this write path (release) must use the identical
|
|
585
|
+
* canonical-key comparison, or a claim written under an explicit-override actor (`actor_key`
|
|
586
|
+
* bare, e.g. `"canonical-x"`, but `serializeActor(record.actor)` a DIFFERENT triple, e.g.
|
|
587
|
+
* `"explicit-override:canonical-x:host"`) can be self-recognized by computeEffectiveState() yet
|
|
588
|
+
* fail to release here because the releaser's canonical key was compared against the wrong
|
|
589
|
+
* (re-derived, triple) form instead of the stored actor_key. Comparing two serializeActor()
|
|
590
|
+
* calls unconditionally — as a prior version of this function did — is NOT correct for override
|
|
591
|
+
* actors and reintroduces the exact #291 seam on the release path.
|
|
592
|
+
*
|
|
593
|
+
* Contract: when `releasedBy` is provided AND the existing record is `actor_key`-stamped,
|
|
594
|
+
* `opts.actorKey` is REQUIRED (the canonical `resolveActor(env).actor` string) — otherwise
|
|
595
|
+
* ownership cannot be verified. A caller that passes `releasedBy` without `opts.actorKey` against
|
|
596
|
+
* an `actor_key`-stamped record would have its ownership compared as
|
|
597
|
+
* `existing.actor_key` (bare canonical) vs `serializeActor(releasedBy)` (re-derived triple), which
|
|
598
|
+
* can NEVER match even for the legitimate holder — a silent-failure trap, not a real ownership
|
|
599
|
+
* check. This is refused loudly (see the guard at the top of the `releasedBy` branch below) rather
|
|
600
|
+
* than allowed to silently no-op or wrongly refuse.
|
|
601
|
+
*/
|
|
602
|
+
export function performLocalRelease(
|
|
603
|
+
artifactRoot: string,
|
|
604
|
+
subjectId: string,
|
|
605
|
+
releasedBy: ActorStruct | null,
|
|
606
|
+
opts: { reason?: string; actorKey?: string; tolerateNoActiveClaim?: boolean } = {},
|
|
607
|
+
): AssignmentClaimRecord | null {
|
|
608
|
+
const helper = loadActorIdentityHelper();
|
|
609
|
+
const reason = opts.reason ?? "released";
|
|
610
|
+
const tolerateNoActiveClaim = opts.tolerateNoActiveClaim ?? false;
|
|
570
611
|
|
|
571
612
|
// F1 fix (fix-plan iteration 1, CRITICAL): release mutates the same record file claim/supersede
|
|
572
613
|
// do, under the same per-subject lock (see withSubjectLock()'s doc comment).
|
|
573
|
-
return withSubjectLock(artifactRoot, subjectId, ():
|
|
614
|
+
return withSubjectLock(artifactRoot, subjectId, (): AssignmentClaimRecord | null => {
|
|
574
615
|
const existing = readLocalRecord(artifactRoot, subjectId);
|
|
575
|
-
if (!existing || existing.status !== "claimed")
|
|
616
|
+
if (!existing || existing.status !== "claimed") {
|
|
617
|
+
if (tolerateNoActiveClaim) return null;
|
|
618
|
+
throw new Error(`no active claim to release for subject: ${subjectId}`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (releasedBy) {
|
|
622
|
+
// Contract guard (hardening fix, #292 review): a caller that supplies `releasedBy` but NOT
|
|
623
|
+
// `opts.actorKey` against a record that already carries `actor_key` cannot reliably prove
|
|
624
|
+
// ownership — see this function's doc comment. This is the ONLY combination that fires: it
|
|
625
|
+
// does NOT fire when `existing.actor_key` is absent (the CLI/fixture path, where both sides
|
|
626
|
+
// fall back to serializeActor() and legitimately compare equal). Fail loudly rather than
|
|
627
|
+
// silently no-op (tolerant callers) or wrongly refuse (throwing callers) — never silent.
|
|
628
|
+
if (!opts.actorKey && existing.actor_key) {
|
|
629
|
+
if (tolerateNoActiveClaim) {
|
|
630
|
+
console.error(
|
|
631
|
+
`[performLocalRelease] cannot verify ownership of an actor_key-stamped record without opts.actorKey; skipping release for ${subjectId}`,
|
|
632
|
+
);
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
throw new Error(
|
|
636
|
+
"performLocalRelease: pass opts.actorKey (the canonical resolveActor().actor string) when releasedBy is set and the record carries actor_key — serializeActor(releasedBy) is not a valid ownership key for actor_key-stamped records",
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// AC6: never force-release a claim held by a different actor. Mirrors
|
|
641
|
+
// computeEffectiveState()'s canonical self-recognition comparison EXACTLY —
|
|
642
|
+
// `holderActorKey` prefers the stored `actor_key` (the canonical resolveActor(env).actor
|
|
643
|
+
// string, present on records written by the fixed performLocalClaim/performLocalSupersede
|
|
644
|
+
// paths) and only falls back to `serializeActor(existing.actor)` when `actor_key` is
|
|
645
|
+
// absent (every pre-fix record, every #290 eval fixture). The releaser's side must use the
|
|
646
|
+
// SAME canonical form: `opts.actorKey` (the caller's resolveActor(env).actor string, e.g.
|
|
647
|
+
// scripts/hooks/stop-goal-fit.js's Stop hook) when provided, else re-derived via
|
|
648
|
+
// serializeActor(releasedBy) — never serializeActor() unconditionally on both sides, which
|
|
649
|
+
// would compare the bare actor_key form against a re-derived triple form for an
|
|
650
|
+
// explicit-override actor and spuriously reject a legitimate same-actor release (the #291
|
|
651
|
+
// seam, relocated to this write path).
|
|
652
|
+
const holderActorKey = existing.actor_key || helper.serializeActor(existing.actor);
|
|
653
|
+
const releasedByActorKey = opts.actorKey || helper.serializeActor(releasedBy);
|
|
654
|
+
if (holderActorKey !== releasedByActorKey) {
|
|
655
|
+
if (tolerateNoActiveClaim) return null;
|
|
656
|
+
throw new Error(`--actor-json does not match the current holder (${holderActorKey}); refusing to release a claim held by someone else`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
576
660
|
const record: AssignmentClaimRecord = {
|
|
577
661
|
...existing,
|
|
662
|
+
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
578
663
|
status: "released",
|
|
579
664
|
audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "release", from_actor: existing.actor, to_actor: releasedBy, reason }],
|
|
580
665
|
};
|
|
581
666
|
writeLocalRecord(artifactRoot, subjectId, record);
|
|
582
|
-
|
|
583
|
-
return 0;
|
|
667
|
+
return record;
|
|
584
668
|
});
|
|
585
669
|
}
|
|
586
670
|
|
|
671
|
+
function releaseLocalFile(argv: string[]): number {
|
|
672
|
+
const args = parseArgs(argv);
|
|
673
|
+
const provider = flagString(args.flags, "provider", "local-file");
|
|
674
|
+
if (provider !== "local-file") throw new Error(`release: --provider must be local-file (use render-release for github); got ${provider}`);
|
|
675
|
+
const artifactRoot = requireFlag(args, "artifact-root");
|
|
676
|
+
const subjectId = requireFlag(args, "subject-id");
|
|
677
|
+
const releasedBy = flagString(args.flags, "actor-json") ? loadActorStructFromFile(requireFlag(args, "actor-json")) : null;
|
|
678
|
+
const reason = flagString(args.flags, "reason") ?? "released";
|
|
679
|
+
|
|
680
|
+
const record = performLocalRelease(artifactRoot, subjectId, releasedBy, { reason, tolerateNoActiveClaim: false });
|
|
681
|
+
console.log(JSON.stringify({ role: "AssignmentReleaseResult", subject_id: subjectId, record }, null, 2));
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
587
685
|
/**
|
|
588
686
|
* Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
|
|
589
687
|
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
|
|
@@ -1619,7 +1619,30 @@ function recordAgentEvent(p: ReturnType<typeof parseArgs>): number {
|
|
|
1619
1619
|
if (hasExplicitRoot || !explicit) requireArtifactDirUnderRoot(dir, root);
|
|
1620
1620
|
const timestamp = opt(p, "timestamp", now());
|
|
1621
1621
|
const agent = validateAgentId(opt(p, "agent-id"));
|
|
1622
|
-
|
|
1622
|
+
// #376 model routing: optionally stamp the delegate role/model resolved from
|
|
1623
|
+
// .datum/config.json onto the event so a downstream economics record (#349)
|
|
1624
|
+
// can price role assignments per delegation, and so an escalate-on-gate-failure
|
|
1625
|
+
// re-dispatch records which tier it climbed FROM. Fully additive/optional: when
|
|
1626
|
+
// no routing flag is passed the event shape is byte-identical to before.
|
|
1627
|
+
// These live as TOP-LEVEL event fields (not nested) on purpose: appendJsonl's
|
|
1628
|
+
// serializer (spacedLine) uses the top-level key list as a JSON.stringify array
|
|
1629
|
+
// replacer, which is an allowlist applied at every nesting level — a nested
|
|
1630
|
+
// routing object would have its inner keys stripped. Flat keeps the shape a
|
|
1631
|
+
// simple per-event routing record a JSONL economics feed can read directly.
|
|
1632
|
+
const role = opt(p, "role");
|
|
1633
|
+
const model = opt(p, "model");
|
|
1634
|
+
const escalatedFrom = opt(p, "escalated-from");
|
|
1635
|
+
const event = {
|
|
1636
|
+
timestamp,
|
|
1637
|
+
agent_id: agent,
|
|
1638
|
+
kind: opt(p, "kind", "note"),
|
|
1639
|
+
status: opt(p, "status", "info"),
|
|
1640
|
+
summary: opt(p, "summary"),
|
|
1641
|
+
...(opt(p, "ref") ? { ref: opt(p, "ref") } : {}),
|
|
1642
|
+
...(role ? { role } : {}),
|
|
1643
|
+
...(model ? { model } : {}),
|
|
1644
|
+
...(escalatedFrom ? { escalated_from: escalatedFrom } : {}),
|
|
1645
|
+
};
|
|
1623
1646
|
appendJsonl(path.join(dir, "agents", agent, "events.jsonl"), event);
|
|
1624
1647
|
updateCurrentAgent(root, dir, agent, event.status, timestamp, actorKey);
|
|
1625
1648
|
return 0;
|
|
@@ -2526,52 +2549,118 @@ async function sealCheckpoint(p: ReturnType<typeof parseArgs>): Promise<number>
|
|
|
2526
2549
|
// Called automatically from recordRelease and advanceState→delivered (best-effort).
|
|
2527
2550
|
// Also exposed as the `publish-delivery <artifact-dir>` subcommand for explicit use.
|
|
2528
2551
|
|
|
2552
|
+
/**
|
|
2553
|
+
* #379 supersede-on-publish cleanup: keep delivery/ bounded by pruning inherited PER-SESSION
|
|
2554
|
+
* seal dirs (the growth vector), scoped to avoid any cross-PR conflict.
|
|
2555
|
+
*
|
|
2556
|
+
* An inherited per-session seal dir (`delivery/<other-slug>/`) is UNIQUELY named, so pruning
|
|
2557
|
+
* it can never conflict with a concurrent PR: two branches deleting the same inherited dir is
|
|
2558
|
+
* a delete/delete (auto-merges), and each new delivery adds its OWN distinct
|
|
2559
|
+
* `delivery/<slug>/`. And it is HARMLESS to leave: trust-reconcile.js's prefer-newest
|
|
2560
|
+
* ownership selection always picks THIS session's fresher bundle over an older inherited one.
|
|
2561
|
+
* Pruning is therefore purely to stop unbounded accumulation of permanently-superseded dirs.
|
|
2562
|
+
*
|
|
2563
|
+
* The SHARED FLAT path (`delivery/trust.bundle` + checkpoints) is deliberately NOT pruned
|
|
2564
|
+
* here. During the migration window other concurrent PRs may still seal to the flat path;
|
|
2565
|
+
* deleting it would produce a modify/delete conflict → a DIRTY PR → the no-CI failure this
|
|
2566
|
+
* whole change fixes. The flat path is a single fixed location (not a growth vector) and the
|
|
2567
|
+
* reconciler treats a stale flat bundle as non-owning / older-owning, so leaving it is safe.
|
|
2568
|
+
* Removing the flat legacy seals is a one-time cleanup for a dedicated PR once no open PR
|
|
2569
|
+
* still seals to it — intentionally NOT bundled into every delivery.
|
|
2570
|
+
*
|
|
2571
|
+
* Best-effort: a prune failure is logged, never fatal to the delivery. Never touches
|
|
2572
|
+
* README.md, DECLARED, the flat seal files, or any subdir that is not itself a seal dir.
|
|
2573
|
+
*/
|
|
2574
|
+
function pruneSupersededSeals(deliveryDir: string, keepSlug: string): void {
|
|
2575
|
+
let entries: fs.Dirent[] = [];
|
|
2576
|
+
try {
|
|
2577
|
+
entries = fs.readdirSync(deliveryDir, { withFileTypes: true });
|
|
2578
|
+
} catch {
|
|
2579
|
+
entries = [];
|
|
2580
|
+
}
|
|
2581
|
+
for (const entry of entries) {
|
|
2582
|
+
if (!entry.isDirectory() || entry.name === keepSlug) continue;
|
|
2583
|
+
const subdir = path.join(deliveryDir, entry.name);
|
|
2584
|
+
// Only prune dirs that actually look like a seal dir (contain a trust.bundle or
|
|
2585
|
+
// trust.checkpoint.json) — never an unrelated directory a human placed under delivery/.
|
|
2586
|
+
const looksLikeSeal =
|
|
2587
|
+
fs.existsSync(path.join(subdir, "trust.bundle")) ||
|
|
2588
|
+
fs.existsSync(path.join(subdir, "trust.checkpoint.json"));
|
|
2589
|
+
if (!looksLikeSeal) continue;
|
|
2590
|
+
try {
|
|
2591
|
+
fs.rmSync(subdir, { recursive: true, force: true });
|
|
2592
|
+
process.stderr.write(`[publish-delivery] #379: pruned superseded per-session seal delivery/${entry.name}/ (older-owning/stale; the reconciler selects the newest bundle regardless)\n`);
|
|
2593
|
+
} catch (err) {
|
|
2594
|
+
process.stderr.write(`[publish-delivery] #379: could not prune per-session seal delivery/${entry.name}/ (non-fatal): ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2529
2599
|
/**
|
|
2530
2600
|
* Publish the session's trust artifacts to the committed delivery/ path.
|
|
2531
2601
|
*
|
|
2602
|
+
* #379: writes to a PER-SESSION path `<repoRoot>/delivery/<slug>/` (slug = the session
|
|
2603
|
+
* artifact dir's basename) rather than the shared flat `delivery/`. A shared path guarantees
|
|
2604
|
+
* a git conflict between ANY two concurrent deliveries, and a conflicting (DIRTY) PR gets NO
|
|
2605
|
+
* pull_request workflows — the required Trust Reconcile check silently never runs (field
|
|
2606
|
+
* incidents #330/#358/#378). Per-session paths make concurrent deliveries write DISTINCT
|
|
2607
|
+
* files that never contend. trust-reconcile.js reads both the flat (back-compat) and
|
|
2608
|
+
* per-session layouts via resolveDeliveryCandidates() and selects the owning candidate by
|
|
2609
|
+
* commit ancestry.
|
|
2610
|
+
*
|
|
2532
2611
|
* Copies trust.bundle, trust.checkpoint.json, and (if present)
|
|
2533
|
-
* trust.checkpoint.intoto.json / trust.checkpoint.sig.json
|
|
2534
|
-
* session artifact dir to <repoRoot>/delivery
|
|
2612
|
+
* trust.checkpoint.intoto.json / trust.checkpoint.sig.json / trust.checkpoint.attestation.json
|
|
2613
|
+
* from the session artifact dir to <repoRoot>/delivery/<slug>/.
|
|
2535
2614
|
*
|
|
2536
2615
|
* Fail-soft on a missing bundle: if trust.bundle is absent, returns without throwing.
|
|
2537
2616
|
* Fail-CLOSED on repo-root resolution: repoRoot must be a real, resolved kits/ ancestor
|
|
2538
2617
|
* (see findRepoRootFromDirStrict) — null (no ancestor found) skips the publish with a
|
|
2539
2618
|
* visible warning instead of writing to whatever process.cwd() happens to be. This
|
|
2540
2619
|
* prevents a scratch/test session dir (no kits/ ancestor) from silently clobbering an
|
|
2541
|
-
* unrelated real repo's delivery/
|
|
2620
|
+
* unrelated real repo's delivery/ seal when invoked with that repo as cwd (see
|
|
2542
2621
|
* evals/integration/test_checkpoint_signing.sh TEST 2 and the WS5 session findings at
|
|
2543
2622
|
* .kontourai/flow-agents/ws5-governance-kit-slice1 for the root cause this fixes).
|
|
2544
|
-
* Idempotent: overwrites on re-delivery.
|
|
2623
|
+
* Idempotent: overwrites on re-delivery to the same slug.
|
|
2545
2624
|
*/
|
|
2546
2625
|
export async function publishDelivery(dir: string, repoRoot: string | null): Promise<void> {
|
|
2547
2626
|
const bundleSrc = path.join(dir, "trust.bundle");
|
|
2548
2627
|
if (!fs.existsSync(bundleSrc)) return; // no bundle — skip gracefully
|
|
2549
2628
|
|
|
2550
2629
|
if (!repoRoot) {
|
|
2551
|
-
process.stderr.write(`[publish-delivery] WARNING: no kits/ ancestor found from ${dir}; skipping publish. Refusing to fall back to process.cwd() to avoid clobbering an unrelated repo's delivery/
|
|
2630
|
+
process.stderr.write(`[publish-delivery] WARNING: no kits/ ancestor found from ${dir}; skipping publish. Refusing to fall back to process.cwd() to avoid clobbering an unrelated repo's delivery/ seal. Pass --repo-root explicitly if this session dir is intentionally outside a repo checkout.\n`);
|
|
2552
2631
|
return;
|
|
2553
2632
|
}
|
|
2554
2633
|
|
|
2555
2634
|
const deliveryDir = path.join(repoRoot, "delivery");
|
|
2635
|
+
// #379: slug is the session artifact dir's basename — the same human-meaningful id used
|
|
2636
|
+
// throughout the session (.kontourai/flow-agents/<slug>/). The per-session dir NAME is only
|
|
2637
|
+
// a collision-avoidance handle; ownership is decided by commit ancestry, not by name.
|
|
2638
|
+
const slug = path.basename(path.resolve(dir));
|
|
2639
|
+
const sessionDeliveryDir = path.join(deliveryDir, slug);
|
|
2640
|
+
|
|
2556
2641
|
fs.mkdirSync(deliveryDir, { recursive: true });
|
|
2642
|
+
// Supersede inherited/flat seals BEFORE writing this session's dir (keepSlug = our own).
|
|
2643
|
+
pruneSupersededSeals(deliveryDir, slug);
|
|
2644
|
+
fs.mkdirSync(sessionDeliveryDir, { recursive: true });
|
|
2557
2645
|
|
|
2558
2646
|
// Required: trust.bundle (the CI anchor)
|
|
2559
|
-
fs.copyFileSync(bundleSrc, path.join(
|
|
2647
|
+
fs.copyFileSync(bundleSrc, path.join(sessionDeliveryDir, "trust.bundle"));
|
|
2560
2648
|
|
|
2561
2649
|
// Optional companions: checkpoint + signing artifacts
|
|
2562
2650
|
const companions = [
|
|
2563
2651
|
"trust.checkpoint.json",
|
|
2564
2652
|
"trust.checkpoint.intoto.json",
|
|
2565
2653
|
"trust.checkpoint.sig.json",
|
|
2654
|
+
"trust.checkpoint.attestation.json",
|
|
2566
2655
|
];
|
|
2567
2656
|
for (const filename of companions) {
|
|
2568
2657
|
const src = path.join(dir, filename);
|
|
2569
2658
|
if (fs.existsSync(src)) {
|
|
2570
|
-
fs.copyFileSync(src, path.join(
|
|
2659
|
+
fs.copyFileSync(src, path.join(sessionDeliveryDir, filename));
|
|
2571
2660
|
}
|
|
2572
2661
|
}
|
|
2573
2662
|
|
|
2574
|
-
process.stderr.write(`[publish-delivery] published trust.bundle and companions to ${
|
|
2663
|
+
process.stderr.write(`[publish-delivery] published trust.bundle and companions to ${sessionDeliveryDir} (per-session path, #379)\n`);
|
|
2575
2664
|
}
|
|
2576
2665
|
|
|
2577
2666
|
/**
|
|
@@ -3330,7 +3419,7 @@ function loadLivenessReadHelper(): {
|
|
|
3330
3419
|
// init-plan claims the work-item; advance-state heartbeats (or releases on terminal),
|
|
3331
3420
|
// so the workflow lifecycle itself maintains the liveness claim — no manual liveness calls.
|
|
3332
3421
|
// Additive + fail-open: a liveness-emit failure never affects the workflow command.
|
|
3333
|
-
const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
3422
|
+
export const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
3334
3423
|
/**
|
|
3335
3424
|
* Delegate to the shared pure-CJS resolver (scripts/hooks/lib/actor-identity.js), mirroring the
|
|
3336
3425
|
* createRequire pattern used by readLivenessEvents() above. Deliberately NO inline duplicate
|