@kontourai/flow-agents 3.0.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 +12 -0
- package/CHANGELOG.md +32 -0
- package/CONTEXT.md +67 -1
- package/README.md +4 -0
- package/build/src/cli/assignment-provider.d.ts +188 -0
- package/build/src/cli/assignment-provider.js +193 -51
- package/build/src/cli/workflow-sidecar.d.ts +16 -6
- package/build/src/cli/workflow-sidecar.js +492 -45
- package/build/src/lib/flow-resolver.d.ts +12 -6
- package/build/src/lib/flow-resolver.js +30 -14
- package/build/src/tools/validate-source-tree.js +2 -1
- package/context/contracts/assignment-provider-contract.md +10 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/contracts/probe-docs-write-contract.md +187 -0
- package/context/scripts/hooks/config-protection.js +24 -4
- package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/context/scripts/hooks/stop-goal-fit.js +262 -5
- package/context/scripts/hooks/workflow-steering.js +42 -0
- package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
- package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
- package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0006-typescript-first-source-policy.md +2 -0
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0007-skill-audit.md +2 -0
- package/docs/adr/0008-kit-operation-boundary.md +2 -0
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
- package/docs/adr/0011-mcp-posture.md +2 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
- package/docs/adr/0013-context-lifecycle.md +2 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
- package/docs/adr/0016-three-hard-boundary-model.md +2 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
- package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
- package/docs/adr/0019-kit-dependency-ownership.md +2 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +113 -0
- package/docs/adr/README.md +49 -0
- package/docs/adr/index.md +34 -0
- package/docs/decisions/agent-coordination.md +20 -0
- package/docs/decisions/anti-gaming-trust-security.md +20 -0
- package/docs/decisions/context-lifecycle.md +18 -0
- package/docs/decisions/core-domain-kit-boundary.md +18 -0
- package/docs/decisions/flow-flow-agents-boundary.md +18 -0
- package/docs/decisions/flow-kit.md +20 -0
- package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
- package/docs/decisions/graph-knowledge-provider.md +63 -0
- package/docs/decisions/hook-core-kit-boundary.md +18 -0
- package/docs/decisions/index.md +19 -0
- package/docs/decisions/kit-dependency-ownership.md +18 -0
- package/docs/decisions/kit-operation-boundary.md +18 -0
- package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
- package/docs/decisions/kontour-resource-contract.md +18 -0
- package/docs/decisions/mcp-posture.md +18 -0
- package/docs/decisions/three-hard-boundary-model.md +18 -0
- package/docs/decisions/trust-reconcile.md +20 -0
- package/docs/decisions/typescript-source-policy.md +48 -0
- package/docs/decisions/workflow-enforcement.md +18 -0
- package/docs/decisions/workflow-trust-state.md +20 -0
- package/docs/fixture-ownership.md +1 -1
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
- package/evals/integration/test_checkpoint_signing.sh +4 -3
- package/evals/integration/test_current_json_per_actor.sh +516 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
- package/evals/integration/test_gate_lockdown.sh +46 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +14 -6
- package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
- package/evals/run.sh +10 -0
- package/evals/static/test_knowledge_providers.sh +13 -4
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/evals/static/test_workflow_skills.sh +15 -2
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/deliver/SKILL.md +69 -11
- package/kits/builder/skills/design-probe/SKILL.md +47 -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/pickup-probe/SKILL.md +4 -4
- 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 +147 -18
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +97 -14
- package/kits/knowledge/adapters/shared/codec.js +265 -0
- package/kits/knowledge/docs/README.md +121 -2
- package/kits/knowledge/docs/store-contract.md +517 -7
- 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 +212 -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/kits/knowledge/flows/promote.flow.json +84 -0
- package/kits/knowledge/kit.json +15 -0
- package/kits/knowledge/promote/distill.js +96 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
- package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
- package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
- package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
- package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
- package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
- package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
- package/kits/knowledge/promote/fixtures/session/state.json +19 -0
- package/kits/knowledge/promote/health.js +137 -0
- package/kits/knowledge/promote/index.js +176 -0
- package/kits/knowledge/promote/ingest.js +103 -0
- package/kits/knowledge/promote/lib.js +132 -0
- package/kits/knowledge/promote/link.js +84 -0
- package/kits/knowledge/promote/promote.test.js +174 -0
- package/kits/knowledge/providers/conformance/suite.test.js +18 -0
- package/kits/knowledge/providers/index.js +1 -0
- package/kits/knowledge/providers/neo4j/connection.js +121 -0
- package/kits/knowledge/providers/neo4j/cypher.js +190 -0
- package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
- package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
- package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
- package/kits/knowledge/providers/neo4j/index.js +280 -0
- package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
- package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
- package/kits/knowledge/providers/neo4j/sync.js +235 -0
- package/package.json +4 -2
- package/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/README.md +1 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +144 -26
- package/scripts/freeze-adrs.mjs +364 -0
- package/scripts/hooks/config-protection.js +24 -4
- package/scripts/hooks/evidence-capture.js +4 -1
- package/scripts/hooks/lib/config-protection-remedies.js +9 -0
- package/scripts/hooks/lib/current-pointer.js +123 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
- package/scripts/hooks/stop-goal-fit.js +262 -5
- package/scripts/hooks/workflow-steering.js +42 -0
- package/scripts/statusline/flow-agents-statusline.js +3 -1
- package/src/cli/assignment-provider.ts +239 -59
- package/src/cli/workflow-sidecar.ts +529 -43
- package/src/lib/flow-resolver.ts +35 -14
- package/src/tools/validate-source-tree.ts +2 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { execFileSync } from "node:child_process";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
@@ -8,6 +9,11 @@ import { fileURLToPath } from "node:url";
|
|
|
8
9
|
// ADR 0016 Abstraction A: shared FlowDefinition resolver (P-a)
|
|
9
10
|
import { resolveActiveFlowStep, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy } from "../lib/flow-resolver.js";
|
|
10
11
|
import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
|
|
12
|
+
// #291 Wave 1 Task 1.1 exports: ensure-session's ownership guard reuses the EXACT same
|
|
13
|
+
// assignment ⋈ liveness join / claim / supersede logic #290 already ships for the
|
|
14
|
+
// `assignment-provider` CLI, rather than reimplementing a second, parallel join (static ESM
|
|
15
|
+
// import — same idiom already used above for ../lib/flow-resolver.js).
|
|
16
|
+
import { computeEffectiveState, performLocalClaim, performLocalSupersede, readLocalAssignmentStatus } from "./assignment-provider.js";
|
|
11
17
|
export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
|
|
12
18
|
export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
|
|
13
19
|
export const checkKinds = new Set(["build", "types", "lint", "test", "command", "security", "diff", "browser", "runtime", "policy", "external"]);
|
|
@@ -177,6 +183,74 @@ function validateExplicitBranch(value) {
|
|
|
177
183
|
fail(gitMessage ? `git check-ref-format rejected the value: ${gitMessage}` : "git check-ref-format rejected the value");
|
|
178
184
|
}
|
|
179
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* #291 Wave 2 Task 2.1: the single shared actor resolver for ensure-session — used by BOTH
|
|
188
|
+
* resolveSessionBranch() (below) and the new ownership guard inside ensureSession(), so the
|
|
189
|
+
* branch-naming actor and the assignment-claim actor are ALWAYS derived from the same one
|
|
190
|
+
* resolution pass (never re-derived twice with any risk of divergence). Same explicit-actor
|
|
191
|
+
* validation/die behavior as before this refactor (garbage --actor still dies; ambiguous/
|
|
192
|
+
* unresolved actor never hard-fails session creation — Design Decision 4).
|
|
193
|
+
*
|
|
194
|
+
* Returns TWO distinct identity strings, not one, because this repo already has two genuinely
|
|
195
|
+
* different actor-identity conventions in play and reconciling them into a single string would
|
|
196
|
+
* either break existing branch-naming byte-for-byte (see test_workflow_sidecar_writer.sh AC4) or
|
|
197
|
+
* break self-recognition against a durable assignment-claim record:
|
|
198
|
+
* - `branchActorKey`: the flat, single-token-or-triple string this file has ALWAYS used for
|
|
199
|
+
* branch naming, liveness `--actor`, and (Wave 2 Task 2.1 §6) per-actor current.json
|
|
200
|
+
* partitioning — exactly `resolveActor(env).actor`, or the bare sanitized explicit --actor
|
|
201
|
+
* override, unchanged from before this refactor.
|
|
202
|
+
* - `actorStruct` / `claimActorKey` (`= serializeActor(actorStruct)`): a structured ActorStruct
|
|
203
|
+
* for the assignment-provider claim record's `actor` field (Wave 1's `performLocalClaim`/
|
|
204
|
+
* `performLocalSupersede` require one), and the identity string passed as `computeEffectiveState`'s
|
|
205
|
+
* `selfActor` param. For the common/default case (no explicit --actor override — the ambient
|
|
206
|
+
* runtime-session-id or process-ancestry derivation), this is reconstructed from the SAME
|
|
207
|
+
* exported primitives `resolveActor()` uses internally, so `claimActorKey` reproduces
|
|
208
|
+
* `branchActorKey` bit-for-bit. For an explicit --actor override, no ambient ActorStruct
|
|
209
|
+
* exists (resolveActor()'s override branch is a flat bypass, never a serializeActor() triple),
|
|
210
|
+
* so a synthetic-but-deterministic wrapper is used instead — self-recognition on reentry still
|
|
211
|
+
* holds (same override value always re-derives the same claimActorKey), but this synthetic key
|
|
212
|
+
* will not equal a DIFFERENT process's flat `liveness claim --actor <sameOverrideValue>` actor
|
|
213
|
+
* string (that command's own `--actor` bypass strips the same override value's ':' or wraps it
|
|
214
|
+
* no differently — a pre-existing seam between the liveness-actor and assignment-actor
|
|
215
|
+
* identity domains, not introduced by this task; see Wave 2 Task 2.1 plan Conflict #3).
|
|
216
|
+
*/
|
|
217
|
+
function resolveEnsureSessionActor(p) {
|
|
218
|
+
const helper = loadActorIdentityHelper();
|
|
219
|
+
const explicitActorRaw = opt(p, "actor", "").trim();
|
|
220
|
+
if (explicitActorRaw && !/[A-Za-z0-9_.-]/.test(explicitActorRaw)) {
|
|
221
|
+
die("ensure-session --actor value strips to empty under the allowed actor charset ([A-Za-z0-9_.-]) — pass a value containing at least one letter, digit, underscore, period, or hyphen.");
|
|
222
|
+
}
|
|
223
|
+
const explicitActor = explicitActorRaw ? helper.sanitizeSegment(explicitActorRaw) : "";
|
|
224
|
+
const resolved = explicitActor ? { actor: explicitActor, source: "explicit-override" } : helper.resolveActor(process.env);
|
|
225
|
+
const unresolved = helper.isUnresolvedActor(resolved.actor);
|
|
226
|
+
const branchActorKey = unresolved ? `unknown-actor-${unknownDisambiguator(resolved.actor)}` : resolved.actor;
|
|
227
|
+
if (unresolved) {
|
|
228
|
+
// Design Decision 4 (unchanged): never hard-fail session creation on actor ambiguity. No real
|
|
229
|
+
// ActorStruct exists for "unresolved" — the guard (ensureSession) is responsible for skipping
|
|
230
|
+
// the ownership guard entirely when unresolved, rather than claiming under a synthetic identity.
|
|
231
|
+
return { actorStruct: { runtime: "unresolved", session_id: branchActorKey, host: os.hostname() }, actorKey: resolved.actor, branchActorKey, unresolved: true };
|
|
232
|
+
}
|
|
233
|
+
const actorStruct = resolved.source === "explicit-override"
|
|
234
|
+
? { runtime: "explicit-override", session_id: resolved.actor, host: os.hostname() }
|
|
235
|
+
: { runtime: helper.detectRuntime(process.env), session_id: helper.runtimeSessionId(process.env) || (() => { const seed = helper.ancestorActorSeed(); return seed ? `anc-${seed}` : ""; })(), host: os.hostname() };
|
|
236
|
+
const actorKey = helper.serializeActor(actorStruct);
|
|
237
|
+
return { actorStruct, actorKey, branchActorKey, unresolved: false };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Resolve an actor key for a READ-ONLY consumer (current(), currentDir(), recordAgentEvent(),
|
|
241
|
+
* writeTrustBundle()'s default) — deliberately lenient (never
|
|
242
|
+
* dies on a garbage --actor value; that enforcement belongs only to the write/claim path in
|
|
243
|
+
* resolveEnsureSessionActor above). Falls back to resolveActor(process.env) exactly like every
|
|
244
|
+
* write path already does. The returned value is passed straight into
|
|
245
|
+
* scripts/hooks/lib/current-pointer.js's readCurrentPointer(), which already tolerates an empty
|
|
246
|
+
* or unresolved actor by falling straight through to the legacy current.json branch — so an
|
|
247
|
+
* unresolved actor here reproduces exactly today's (pre-#291) legacy-only read behavior.
|
|
248
|
+
*/
|
|
249
|
+
function resolveReadActorKey(p) {
|
|
250
|
+
const helper = loadActorIdentityHelper();
|
|
251
|
+
const explicit = opt(p, "actor", "").trim();
|
|
252
|
+
return explicit ? helper.sanitizeSegment(explicit) : helper.resolveActor(process.env).actor;
|
|
253
|
+
}
|
|
180
254
|
/** Resolve the branch to seed a brand-new session with. Only called from ensureSession's
|
|
181
255
|
* `if (!md)` fresh-creation branch — an existing session's already-recorded branch is never
|
|
182
256
|
* recomputed (ADR 0021 §5 takeover continuity; see Design Decision 3). Precedence: explicit
|
|
@@ -193,13 +267,12 @@ function resolveSessionBranch(p, slug) {
|
|
|
193
267
|
return explicitBranch;
|
|
194
268
|
}
|
|
195
269
|
const helper = loadActorIdentityHelper();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const safeActor = unresolved ? `unknown-actor-${unknownDisambiguator(actor)}` : sanitizeBranchSegment(actor, helper);
|
|
270
|
+
// #291 Wave 2 Task 2.1: actor resolution is now single-sourced via resolveEnsureSessionActor()
|
|
271
|
+
// (shared with the new ownership guard) — branchActorKey reproduces EXACTLY what this function's
|
|
272
|
+
// own inline `actor` variable used to compute, so branch naming is byte-identical to before this
|
|
273
|
+
// refactor (proven by test_workflow_sidecar_writer.sh's AC2/AC4/AC5 branch-naming assertions).
|
|
274
|
+
const { branchActorKey, unresolved } = resolveEnsureSessionActor(p);
|
|
275
|
+
const safeActor = unresolved ? branchActorKey : sanitizeBranchSegment(branchActorKey, helper);
|
|
203
276
|
if (unresolved)
|
|
204
277
|
process.stderr.write("[ensure-session] actor unresolved; branch uses \"unknown-actor\" segment (set --actor or FLOW_AGENTS_ACTOR for a stable branch name)\n");
|
|
205
278
|
return `agent/${safeActor}/${sanitizeBranchSegment(slug, helper)}`;
|
|
@@ -403,7 +476,7 @@ function critiqueToEventStatus(verdict, findings) {
|
|
|
403
476
|
* @param critiques Critique objects (from critique.json .critiques array)
|
|
404
477
|
* @param commandLog Optional parsed command-log.jsonl entries (capture-authoritative fold)
|
|
405
478
|
*/
|
|
406
|
-
export async function buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, flowAgentsDir) {
|
|
479
|
+
export async function buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, flowAgentsDir, actorKey) {
|
|
407
480
|
const surface = await tryLoadSurface();
|
|
408
481
|
if (!surface)
|
|
409
482
|
return null;
|
|
@@ -413,7 +486,10 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
413
486
|
// each produced claim gets a DECLARED primary claim (kit-typed) plus a legacy shadow
|
|
414
487
|
// (workflow.* type, claimId suffix "-legacy") for backward compatibility. When null,
|
|
415
488
|
// only the existing workflow.* claims are produced (zero behavior change).
|
|
416
|
-
|
|
489
|
+
// #291 Wave 2 Task 2.1 (§7)/Task 2.2: actorKey (when the caller already resolved one — see
|
|
490
|
+
// writeTrustBundle below) threads through to resolveActiveFlowStep's per-actor-first,
|
|
491
|
+
// legacy-fallback current.json read; omitted, this is IDENTICAL to pre-#291 behavior.
|
|
492
|
+
const activeStep = flowAgentsDir ? resolveActiveFlowStep(flowAgentsDir, actorKey) : null;
|
|
417
493
|
const claims = [];
|
|
418
494
|
const evidenceItems = [];
|
|
419
495
|
const events = [];
|
|
@@ -721,7 +797,7 @@ export async function buildTrustBundle(slug, timestamp, checks, criteria, critiq
|
|
|
721
797
|
* @param criteria Acceptance criteria objects (same as buildTrustBundle)
|
|
722
798
|
* @param critiques Critique objects (same as buildTrustBundle)
|
|
723
799
|
*/
|
|
724
|
-
export async function writeTrustBundle(dir, slug, timestamp, checks, criteria, critiques) {
|
|
800
|
+
export async function writeTrustBundle(dir, slug, timestamp, checks, criteria, critiques, actorKey) {
|
|
725
801
|
try {
|
|
726
802
|
// Fold the deterministic capture log (PostToolUse evidence-capture) into the
|
|
727
803
|
// bundle so capture is authoritative over claimed status. Best-effort read.
|
|
@@ -739,17 +815,23 @@ export async function writeTrustBundle(dir, slug, timestamp, checks, criteria, c
|
|
|
739
815
|
// ADR 0016 Abstraction A (P-d): pass the runtime artifact root ONLY when current.json
|
|
740
816
|
// points to this session (scoped active-flow guard). If current.json.artifact_dir
|
|
741
817
|
// resolves to a different session, pass null — no active-flow claim mapping for this bundle.
|
|
818
|
+
// #291 Wave 2 Task 2.1 (§7): the SOURCE of "what does current.json say" now prefers the
|
|
819
|
+
// resolved actor's own per-actor projection (falling back to the legacy global file) via the
|
|
820
|
+
// shared readCurrentPointer() helper — the "does artifact_dir match dir?" comparison itself is
|
|
821
|
+
// UNCHANGED. actorKey defaults to resolveActor(process.env) when the caller (below) does not
|
|
822
|
+
// already have one resolved, exactly like every other read-path consumer in this file.
|
|
742
823
|
const _flowAgentsDir = path.dirname(dir);
|
|
824
|
+
const _effectiveActorKey = actorKey ?? loadActorIdentityHelper().resolveActor(process.env).actor;
|
|
743
825
|
let _scopedFlowAgentsDir = undefined;
|
|
744
826
|
try {
|
|
745
|
-
const _currentRaw =
|
|
746
|
-
const _artDir = typeof _currentRaw["artifact_dir"] === "string" ? _currentRaw["artifact_dir"] : null;
|
|
827
|
+
const _currentRaw = loadCurrentPointerHelper().readCurrentPointer(_flowAgentsDir, _effectiveActorKey).payload;
|
|
828
|
+
const _artDir = _currentRaw && typeof _currentRaw["artifact_dir"] === "string" ? _currentRaw["artifact_dir"] : null;
|
|
747
829
|
if (_artDir && path.resolve(_flowAgentsDir, _artDir) === path.resolve(dir)) {
|
|
748
830
|
_scopedFlowAgentsDir = _flowAgentsDir;
|
|
749
831
|
}
|
|
750
832
|
}
|
|
751
833
|
catch { /* current.json absent or unreadable — no scoping */ }
|
|
752
|
-
const bundle = await buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, _scopedFlowAgentsDir);
|
|
834
|
+
const bundle = await buildTrustBundle(slug, timestamp, checks, criteria, critiques, commandLog, _scopedFlowAgentsDir, _effectiveActorKey);
|
|
753
835
|
if (!bundle)
|
|
754
836
|
return { written: false, errors: [] }; // Surface unavailable — fail-open, skip write
|
|
755
837
|
const result = await validateTrustBundle(bundle);
|
|
@@ -1110,12 +1192,34 @@ function resolveFirstStep(flowId, repoRoot) {
|
|
|
1110
1192
|
return null;
|
|
1111
1193
|
}
|
|
1112
1194
|
}
|
|
1113
|
-
|
|
1195
|
+
/**
|
|
1196
|
+
* Delegate to the shared pure-CJS per-actor current-pointer helper
|
|
1197
|
+
* (scripts/hooks/lib/current-pointer.js), mirroring the createRequire pattern already used above
|
|
1198
|
+
* by loadActorIdentityHelper()/loadLivenessWriteHelper() for cross-boundary CJS helper reuse
|
|
1199
|
+
* (#291 Wave 2 Task 2.1). This is the ONE place every current.json reader/writer in this file
|
|
1200
|
+
* goes through from here on — the per-actor-first/legacy-fallback compat-shim rule (and its
|
|
1201
|
+
* write-side counterpart) can never drift between call sites.
|
|
1202
|
+
*/
|
|
1203
|
+
function loadCurrentPointerHelper() {
|
|
1204
|
+
const _req = createRequire(import.meta.url);
|
|
1205
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../scripts/hooks/lib/current-pointer.js");
|
|
1206
|
+
return _req(helperPath);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* #291 Wave 2 Task 2.1 (§5): writes the UNCHANGED legacy global `<root>/current.json` (the
|
|
1210
|
+
* compat-shim's write-side half — every existing consumer without an actorKey keeps reading
|
|
1211
|
+
* exactly this file, exactly as before this change), then ADDITIONALLY projects the SAME payload
|
|
1212
|
+
* into the caller's own `current/<actor>.json` when `actorKey` resolves to a real (not
|
|
1213
|
+
* unresolved) actor. When `actorKey` is omitted or unresolved, only the legacy file is written —
|
|
1214
|
+
* byte-identical to this function's pre-#291 behavior — with a stderr note mirroring the existing
|
|
1215
|
+
* unresolved-actor branch-naming diagnostic.
|
|
1216
|
+
*/
|
|
1217
|
+
function writeCurrent(root, dir, timestamp, owner, source, flowId, stepId, adHocReason, actorKey) {
|
|
1114
1218
|
// #289: mirror the active session's already-recorded branch (state.json.branch) into
|
|
1115
1219
|
// current.json so consumers of current.json (which has no schema of its own — not one of the
|
|
1116
1220
|
// 9 schemas under schemas/) see the routing branch without re-reading state.json separately.
|
|
1117
1221
|
const branch = loadJson(path.join(dir, "state.json")).branch;
|
|
1118
|
-
|
|
1222
|
+
const payload = {
|
|
1119
1223
|
schema_version: "1.0",
|
|
1120
1224
|
active_slug: path.basename(dir),
|
|
1121
1225
|
artifact_dir: path.relative(root, dir) || ".",
|
|
@@ -1134,7 +1238,21 @@ function writeCurrent(root, dir, timestamp, owner, source, flowId, stepId, adHoc
|
|
|
1134
1238
|
// gate-review can distinguish an intentional direct entry (e.g. a planning-only
|
|
1135
1239
|
// session that skips pull-work) from a stale/mis-stamped active_step_id.
|
|
1136
1240
|
...(adHocReason ? { ad_hoc_entry: true, ad_hoc_reason: adHocReason } : {}),
|
|
1137
|
-
}
|
|
1241
|
+
};
|
|
1242
|
+
writeJson(path.join(root, "current.json"), payload);
|
|
1243
|
+
if (actorKey && !loadActorIdentityHelper().isUnresolvedActor(actorKey)) {
|
|
1244
|
+
try {
|
|
1245
|
+
loadCurrentPointerHelper().writePerActorCurrent(root, actorKey, payload);
|
|
1246
|
+
}
|
|
1247
|
+
catch (err) {
|
|
1248
|
+
// Best-effort projection only — the legacy file above is already durable and authoritative;
|
|
1249
|
+
// a failure here must never affect ensure-session's own exit code (fail-open, visible).
|
|
1250
|
+
process.stderr.write(`[ensure-session] failed to write per-actor current pointer: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
else {
|
|
1254
|
+
process.stderr.write("[ensure-session] actor unresolved or not provided; per-actor current.json projection skipped (legacy current.json remains authoritative)\n");
|
|
1255
|
+
}
|
|
1138
1256
|
}
|
|
1139
1257
|
function loadCurrent(root) {
|
|
1140
1258
|
const file = path.join(root, "current.json");
|
|
@@ -1142,8 +1260,16 @@ function loadCurrent(root) {
|
|
|
1142
1260
|
return null;
|
|
1143
1261
|
return JSON.parse(read(file));
|
|
1144
1262
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1263
|
+
/**
|
|
1264
|
+
* #291 Wave 2 Task 2.1 (§6): resolves the active session directory via the shared
|
|
1265
|
+
* per-actor-first/legacy-fallback compat shim (readCurrentPointer) instead of the unconditional
|
|
1266
|
+
* legacy-only loadCurrent() this function used before #291. When `actorKey` is omitted, empty, or
|
|
1267
|
+
* unresolved, `readCurrentPointer` falls straight through to the legacy file — IDENTICAL to this
|
|
1268
|
+
* function's pre-#291 behavior.
|
|
1269
|
+
*/
|
|
1270
|
+
function currentDir(root, actorKey) {
|
|
1271
|
+
const pointer = loadCurrentPointerHelper().readCurrentPointer(root, actorKey);
|
|
1272
|
+
const c = pointer.payload;
|
|
1147
1273
|
if (!c)
|
|
1148
1274
|
return null;
|
|
1149
1275
|
const dir = path.resolve(root, c.artifact_dir ?? c.active_slug ?? "");
|
|
@@ -1157,16 +1283,38 @@ function currentDir(root) {
|
|
|
1157
1283
|
}
|
|
1158
1284
|
return dir;
|
|
1159
1285
|
}
|
|
1160
|
-
|
|
1286
|
+
/**
|
|
1287
|
+
* #291 Wave 2 Task 2.1 (§6): updates BOTH the legacy current.json (when IT points at `dir` — the
|
|
1288
|
+
* exact, unchanged existing check/write) AND the resolved actor's own per-actor current.json
|
|
1289
|
+
* (independently, when IT points at `dir`) — never silently drops the legacy-file update path.
|
|
1290
|
+
* The two checks are independent: either, both, or neither may fire depending on which
|
|
1291
|
+
* pointer(s) currently reference `dir`.
|
|
1292
|
+
*/
|
|
1293
|
+
function updateCurrentAgent(root, dir, agentId, status, timestamp, actorKey) {
|
|
1294
|
+
const applyAgentUpdate = (payload) => {
|
|
1295
|
+
const active = Array.isArray(payload.active_agents) ? payload.active_agents.filter((a) => a.agent_id !== agentId) : [];
|
|
1296
|
+
if (status === "active" || status === "blocked")
|
|
1297
|
+
active.push({ agent_id: agentId, status, updated_at: timestamp });
|
|
1298
|
+
return { ...payload, active_agents: active, updated_at: timestamp };
|
|
1299
|
+
};
|
|
1161
1300
|
const cur = loadCurrent(root);
|
|
1162
|
-
if (
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if (
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1301
|
+
if (cur && path.resolve(root, cur.artifact_dir ?? "") === path.resolve(dir)) {
|
|
1302
|
+
writeJson(path.join(root, "current.json"), applyAgentUpdate(cur));
|
|
1303
|
+
}
|
|
1304
|
+
if (actorKey && !loadActorIdentityHelper().isUnresolvedActor(actorKey)) {
|
|
1305
|
+
const helper = loadCurrentPointerHelper();
|
|
1306
|
+
const perActorFile = helper.perActorCurrentFile(root, actorKey);
|
|
1307
|
+
let perActor = null;
|
|
1308
|
+
try {
|
|
1309
|
+
perActor = fs.existsSync(perActorFile) ? JSON.parse(fs.readFileSync(perActorFile, "utf8")) : null;
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
perActor = null;
|
|
1313
|
+
}
|
|
1314
|
+
if (perActor && path.resolve(root, perActor.artifact_dir ?? "") === path.resolve(dir)) {
|
|
1315
|
+
helper.writePerActorCurrent(root, actorKey, applyAgentUpdate(perActor));
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1170
1318
|
}
|
|
1171
1319
|
function initSidecars(dir, slug, sourceRequest, summary, nextAction, timestamp, markdown) {
|
|
1172
1320
|
const criteria = markdown ? definitionAcceptanceLines(markdown).map(parseCriterion) : [];
|
|
@@ -1214,10 +1362,191 @@ function initSidecars(dir, slug, sourceRequest, summary, nextAction, timestamp,
|
|
|
1214
1362
|
...sidecarBase(slug), summary, current_state_ref: "state.json", next_steps: nextAction ? [nextAction] : [], blockers: [], warnings: [],
|
|
1215
1363
|
});
|
|
1216
1364
|
}
|
|
1365
|
+
/** Read a `--*-json` flag's value as a file path (or `-` for stdin), mirroring
|
|
1366
|
+
* assignment-provider.ts's own `loadJsonInput` convention — this file's OTHER `--*-json` flags
|
|
1367
|
+
* (e.g. `--check-json`) instead take a literal inline JSON string via parseJson(), a DIFFERENT
|
|
1368
|
+
* convention; `--effective-state-json` deliberately follows assignment-provider's file/stdin
|
|
1369
|
+
* convention instead, since its value is the literal JSON `assignment-provider status
|
|
1370
|
+
* --provider github ...` already prints to a file (or pipe), not something a caller would want to
|
|
1371
|
+
* inline as a shell argument. */
|
|
1372
|
+
function loadJsonInputFile(file) {
|
|
1373
|
+
return file === "-" ? JSON.parse(fs.readFileSync(0, "utf8")) : JSON.parse(read(file));
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* #291 Wave 2 Task 2.1 (§4): ensure-session's pre-entry ownership guard. MUST be called before
|
|
1377
|
+
* any directory or file is created (a refusal must never leave a stray empty session dir) —
|
|
1378
|
+
* ensureSession() calls this immediately, before `fs.mkdirSync(dir, ...)`. Reuses #290's
|
|
1379
|
+
* assignment ⋈ liveness join (`computeEffectiveState`, Wave 1 export) so a fresh OTHER-actor claim
|
|
1380
|
+
* refuses entry, a human assignment always asks first (never auto-reclaims), a stale
|
|
1381
|
+
* (`reclaimable`) claim requires the explicit `--supersede-stale` takeover flag, and a `free`
|
|
1382
|
+
* subject establishes a durable claim for the entering actor — ensure-session becomes a SECOND
|
|
1383
|
+
* claim point, alongside pull-work (closing the loophole where a session entered without going
|
|
1384
|
+
* through pull-work never gets a durable claim at all).
|
|
1385
|
+
*
|
|
1386
|
+
* Runs INSIDE ensureSession's existing root-level `withLock` (main(), unchanged) — no second/
|
|
1387
|
+
* competing lock is introduced here. `performLocalClaim`/`performLocalSupersede`'s OWN internal
|
|
1388
|
+
* `withSubjectLock` (a DIFFERENT lockdir, under `<root>/assignment/.<subject>.lockdir`) is what
|
|
1389
|
+
* protects against a concurrent bare `assignment-provider claim` CLI invocation (e.g. from
|
|
1390
|
+
* pull-work) racing this guard — two independent, non-conflicting lock resources.
|
|
1391
|
+
*
|
|
1392
|
+
* Every interpolated actor/holder/assignee/reason field in this function's die() messages is run
|
|
1393
|
+
* through stripControlCharsForDisplay + a 64-char cap (AC9 — the #287/#320/#290 prompt-injection
|
|
1394
|
+
* mitigation class: a hostile liveness event or a hand-crafted --effective-state-json fixture must
|
|
1395
|
+
* never be able to smuggle raw control/ANSI bytes into this process's stderr/thrown message).
|
|
1396
|
+
*/
|
|
1397
|
+
function enforceEnsureSessionOwnership(p, root, slug, dir, resolution) {
|
|
1398
|
+
if (p.flags.has("skip-ownership-guard")) {
|
|
1399
|
+
process.stderr.write("[ensure-session] ownership guard skipped via --skip-ownership-guard\n");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
// Design Decision 4 (unchanged from resolveSessionBranch): actor-resolution ambiguity never
|
|
1403
|
+
// hard-fails session creation. Without a resolvable actor there is no safe identity to claim
|
|
1404
|
+
// under, so the guard is skipped entirely (documented scope boundary, not a silent hole) rather
|
|
1405
|
+
// than claiming under a synthetic/unstable identity.
|
|
1406
|
+
if (resolution.unresolved) {
|
|
1407
|
+
process.stderr.write("[ensure-session] ownership guard not evaluated: actor is unresolved (set --actor or FLOW_AGENTS_ACTOR, or run inside a supported runtime) — proceeding without a durable claim, exactly as ensure-session behaved before #291\n");
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
// F5 fix (fix-plan iteration 1, LOW): match assignment-provider.ts's sanitizeDisplayField
|
|
1411
|
+
// two-tier convention (64 for id-like fields, 240 for free text) rather than asserting one
|
|
1412
|
+
// uniform cap applies to every field class regardless of content. `sanitize` below is 64 chars —
|
|
1413
|
+
// deliberately, not by uniform-cap default — because every value this function actually
|
|
1414
|
+
// interpolates into a die()/stderr message is id-like (holder/actor key, subject slug, assignee,
|
|
1415
|
+
// a claimed_at/last_at timestamp, the provider-kind enum); none of them is free text, so the
|
|
1416
|
+
// id-like tier is the correct (not merely convenient) cap here. sanitizeDisplayField's 240
|
|
1417
|
+
// free-text tier is for fields like a claim record's audit-trail `reason` (see
|
|
1418
|
+
// assignment-provider.ts's sanitizeAuditEntryForDisplay) — this guard never echoes `--reason`
|
|
1419
|
+
// into a die() message, so it has no free-text field requiring the 240 tier today.
|
|
1420
|
+
const sanitize = (value) => stripControlCharsForDisplay(value).slice(0, 64);
|
|
1421
|
+
const nowMs = opt(p, "now") ? Date.parse(opt(p, "now")) : Date.now();
|
|
1422
|
+
const assignmentProviderKind = opt(p, "assignment-provider", "local-file");
|
|
1423
|
+
let effective;
|
|
1424
|
+
const effectiveStateJsonFlag = opt(p, "effective-state-json");
|
|
1425
|
+
if (effectiveStateJsonFlag) {
|
|
1426
|
+
const parsed = loadJsonInputFile(effectiveStateJsonFlag);
|
|
1427
|
+
const candidate = parsed && typeof parsed === "object" ? parsed.effective : undefined;
|
|
1428
|
+
const validStates = new Set(["held", "reclaimable", "human-held", "free"]);
|
|
1429
|
+
if (!candidate || typeof candidate.effective_state !== "string" || !validStates.has(candidate.effective_state)) {
|
|
1430
|
+
die(`ensure-session --effective-state-json must contain an .effective object with a recognized effective_state (held|reclaimable|human-held|free); got ${JSON.stringify(candidate ? candidate.effective_state : candidate)}`);
|
|
1431
|
+
}
|
|
1432
|
+
effective = candidate;
|
|
1433
|
+
}
|
|
1434
|
+
else if (assignmentProviderKind === "local-file") {
|
|
1435
|
+
// Conflict #3 (plan): subjectId for BOTH the assignment lookup and the liveness freshHolders
|
|
1436
|
+
// slug filter is `slug` — the ALREADY-COMPUTED session slug (workItemSlug()'s output), the
|
|
1437
|
+
// exact same identifier pull-work itself uses for both halves of this same join. Never a
|
|
1438
|
+
// second, independently-derived identifier.
|
|
1439
|
+
// F1 fix (fix-plan iteration 1, HIGH): the guard's own self-check and the liveness join must
|
|
1440
|
+
// key on `resolution.branchActorKey` — the canonical `resolveActor(env).actor` string — NOT
|
|
1441
|
+
// `resolution.actorKey` (`serializeActor(actorStruct)`). For an explicit-override actor
|
|
1442
|
+
// (FLOW_AGENTS_ACTOR) those two diverge: `actorKey` serializes to a triple
|
|
1443
|
+
// (`explicit-override:<value>:<host>`) while `branchActorKey` is the bare value every other
|
|
1444
|
+
// tool (`liveness whoami`, `liveness claim --actor`, per-actor current.json, pull-work's
|
|
1445
|
+
// --self-actor) already uses. freshHolders' `selfActor` param and computeEffectiveState's
|
|
1446
|
+
// `selfActor` param must both be the same canonical string the claim record's `actor_key` is
|
|
1447
|
+
// written as (see the performLocalClaim/performLocalSupersede calls below) — otherwise a
|
|
1448
|
+
// fresh heartbeat for this exact actor would never match the join, and
|
|
1449
|
+
// `assignment-provider status --self-actor <branchActorKey>` run by a DIFFERENT tool
|
|
1450
|
+
// afterward would never recognize this guard's own claim as self.
|
|
1451
|
+
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1452
|
+
const events = readLivenessEvents(root);
|
|
1453
|
+
const freshList = loadLivenessReadHelper().freshHolders(events, slug, resolution.branchActorKey, nowMs);
|
|
1454
|
+
effective = computeEffectiveState(assignment, freshList, resolution.branchActorKey, nowMs);
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
// Conflict #5 (plan): GitHub-provider (and any other non-local-file) subjects get no LIVE
|
|
1458
|
+
// guard check here — assignment-provider.ts is deliberately render-don't-execute (no `gh`
|
|
1459
|
+
// calls from any CLI file). A precomputed --effective-state-json is the escape hatch; when
|
|
1460
|
+
// neither applies, this is a documented scope boundary (today's pre-#291 baseline behavior),
|
|
1461
|
+
// never a silent hole.
|
|
1462
|
+
process.stderr.write(`[ensure-session] ownership guard not evaluated: provider "${sanitize(assignmentProviderKind)}" requires --effective-state-json\n`);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const resolveBranchForClaim = () => {
|
|
1466
|
+
const existingBranch = fs.existsSync(path.join(dir, "state.json")) ? loadJson(path.join(dir, "state.json")).branch : undefined;
|
|
1467
|
+
return existingBranch || resolveSessionBranch(p, slug);
|
|
1468
|
+
};
|
|
1469
|
+
switch (effective.effective_state) {
|
|
1470
|
+
case "held": {
|
|
1471
|
+
// F1 fix (fix-plan iteration 1, HIGH): compare against branchActorKey — the same canonical
|
|
1472
|
+
// string just passed as computeEffectiveState's selfActor above — not actorKey (the wrapped
|
|
1473
|
+
// ActorStruct triple), so this redundant belt-and-suspenders check agrees with the
|
|
1474
|
+
// effective_state computation instead of silently using a different identity.
|
|
1475
|
+
const isSelf = effective.reason === "self_is_holder" || (!!effective.holder?.actor && effective.holder.actor === resolution.branchActorKey);
|
|
1476
|
+
if (isSelf)
|
|
1477
|
+
return; // resume own session — no refusal
|
|
1478
|
+
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
1479
|
+
const lastAtSuffix = effective.holder?.last_at ? ` (last_at ${sanitize(effective.holder.last_at)})` : "";
|
|
1480
|
+
die(`ensure-session refused: subject ${sanitize(slug)} is currently held by a different, still-live actor (${holderActor}${lastAtSuffix}). Pick a different work item, or confirm the holder session is truly gone before considering a takeover.`);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
case "human-held": {
|
|
1484
|
+
const assignee = effective.holder?.assignee ? sanitize(effective.holder.assignee) : "an assigned human";
|
|
1485
|
+
const idleSuffix = effective.holder?.idle_days != null ? ` (idle ${Number(effective.holder.idle_days)} day(s))` : "";
|
|
1486
|
+
die(`ensure-session refused: subject ${sanitize(slug)} is assigned to ${assignee}${idleSuffix}. This guard never auto-reclaims a human assignment — confirm with the user before proceeding.`);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
case "reclaimable": {
|
|
1490
|
+
const holderActor = sanitize(effective.holder?.actor ?? "unknown");
|
|
1491
|
+
const claimedAtSuffix = effective.holder?.last_at ? ` (claimed_at ${sanitize(effective.holder.last_at)})` : "";
|
|
1492
|
+
if (!p.flags.has("supersede-stale")) {
|
|
1493
|
+
die(`ensure-session refused: subject ${sanitize(slug)}'s existing claim (held by ${holderActor}${claimedAtSuffix}) is stale. Pass --supersede-stale to take it over explicitly.`);
|
|
1494
|
+
}
|
|
1495
|
+
const assignment = readLocalAssignmentStatus(root, slug);
|
|
1496
|
+
const fromActor = assignment.record?.actor;
|
|
1497
|
+
if (!fromActor)
|
|
1498
|
+
die(`ensure-session --supersede-stale: no existing local-file claim record found for subject ${sanitize(slug)} to supersede`);
|
|
1499
|
+
performLocalSupersede(root, slug, fromActor, resolution.actorStruct, {
|
|
1500
|
+
branch: resolveBranchForClaim(),
|
|
1501
|
+
artifactDir: path.relative(root, dir) || ".",
|
|
1502
|
+
reason: opt(p, "reason", "ensure-session takeover: stale claim"),
|
|
1503
|
+
// F1 fix (fix-plan iteration 1, HIGH): persist the canonical actor_key on the record so
|
|
1504
|
+
// computeEffectiveState's holderActorKey (assignment-provider.ts) matches this same
|
|
1505
|
+
// branchActorKey string on the next status/guard check, cross-tool.
|
|
1506
|
+
actorKey: resolution.branchActorKey,
|
|
1507
|
+
});
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
case "free": {
|
|
1511
|
+
performLocalClaim(root, slug, resolution.actorStruct, {
|
|
1512
|
+
ttlSeconds: opt(p, "claim-ttl-seconds") ? Number(opt(p, "claim-ttl-seconds")) : loadLivenessPolicyHelper().resolveTtlSeconds(process.env),
|
|
1513
|
+
branch: resolveBranchForClaim(),
|
|
1514
|
+
artifactDir: path.relative(root, dir) || ".",
|
|
1515
|
+
reason: opt(p, "reason", "ensure-session entry"),
|
|
1516
|
+
// F1 fix (fix-plan iteration 1, HIGH): see the performLocalSupersede call above.
|
|
1517
|
+
actorKey: resolution.branchActorKey,
|
|
1518
|
+
});
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
default:
|
|
1522
|
+
die(`ensure-session ownership guard: unrecognized effective_state ${JSON.stringify(effective.effective_state)}`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* ensure-session flags added by #291 Wave 2 Task 2.1 (no printed --help/usage text exists in this
|
|
1527
|
+
* file to update — this doc comment is the discoverable reference instead):
|
|
1528
|
+
* --skip-ownership-guard Skip the pre-entry ownership guard entirely (logged, not silent).
|
|
1529
|
+
* --effective-state-json <path|-> Precomputed `{ effective: {...} }` JSON (the exact shape
|
|
1530
|
+
* `assignment-provider status` prints) — required to evaluate the
|
|
1531
|
+
* guard for any --assignment-provider other than local-file.
|
|
1532
|
+
* --assignment-provider <kind> Defaults to "local-file"; see Conflict #5 in the plan for the
|
|
1533
|
+
* GitHub-provider scope boundary.
|
|
1534
|
+
* --now <iso> Deterministic "now" for freshness/idle-day computation (else
|
|
1535
|
+
* Date.now()).
|
|
1536
|
+
* --supersede-stale Required to take over a `reclaimable` (stale) claim.
|
|
1537
|
+
* --claim-ttl-seconds <n> Overrides the liveness-policy TTL default for a new claim.
|
|
1538
|
+
* --reason <text> Audit-trail reason recorded on the claim/supersede record.
|
|
1539
|
+
*/
|
|
1217
1540
|
function ensureSession(p) {
|
|
1218
1541
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : flowAgentsArtifactRoot();
|
|
1219
1542
|
const slug = opt(p, "task-slug") || (opt(p, "work-item") ? workItemSlug(opt(p, "work-item")) : die("--task-slug is required (or pass --work-item to derive it)"));
|
|
1220
1543
|
const dir = sessionDirFor(root, slug);
|
|
1544
|
+
// #291 Wave 2 Task 2.1 (§3, §4): resolve the actor ONCE, then run the ownership guard BEFORE
|
|
1545
|
+
// any directory/file is created — a refusal must never leave a stray empty session dir. Reused
|
|
1546
|
+
// below (writeCurrent's per-actor dual-write) so the branch-naming actor and the
|
|
1547
|
+
// assignment-claim actor are always the same identity.
|
|
1548
|
+
const actorResolution = resolveEnsureSessionActor(p);
|
|
1549
|
+
enforceEnsureSessionOwnership(p, root, slug, dir, actorResolution);
|
|
1221
1550
|
fs.mkdirSync(dir, { recursive: true });
|
|
1222
1551
|
const timestamp = opt(p, "timestamp", now());
|
|
1223
1552
|
let md = fs.existsSync(path.join(dir, `${slug}--deliver.md`)) ? read(path.join(dir, `${slug}--deliver.md`)) : "";
|
|
@@ -1262,13 +1591,18 @@ function ensureSession(p) {
|
|
|
1262
1591
|
adHocReason = opt(p, "ad-hoc-reason") || `direct entry at step "${explicitStep}" via --step-id (flow first step is "${firstStep}")`;
|
|
1263
1592
|
}
|
|
1264
1593
|
}
|
|
1265
|
-
writeCurrent(root, dir, timestamp, "workflow-sidecar", "ensure-session", flowId || undefined, stepId || undefined, adHocReason);
|
|
1594
|
+
writeCurrent(root, dir, timestamp, "workflow-sidecar", "ensure-session", flowId || undefined, stepId || undefined, adHocReason, actorResolution.unresolved ? undefined : actorResolution.branchActorKey);
|
|
1266
1595
|
console.log(dir);
|
|
1267
1596
|
return 0;
|
|
1268
1597
|
}
|
|
1269
1598
|
function current(p) {
|
|
1270
1599
|
const root = opt(p, "artifact-root") ? path.resolve(opt(p, "artifact-root")) : defaultArtifactRootForRead();
|
|
1271
|
-
|
|
1600
|
+
// #291 Wave 2 Task 2.1 (§6): new optional --actor override (same convention as
|
|
1601
|
+
// resolveSessionBranch's existing --actor), falling back to resolveActor(process.env). Only the
|
|
1602
|
+
// SOURCE of "what does current.json say" changes (per-actor-first, legacy-fallback) — this
|
|
1603
|
+
// command's existing --format slug|path output is unchanged.
|
|
1604
|
+
const actorKey = resolveReadActorKey(p);
|
|
1605
|
+
const dir = currentDir(root, actorKey);
|
|
1272
1606
|
if (!dir)
|
|
1273
1607
|
die("no current workflow session is recorded");
|
|
1274
1608
|
const format = opt(p, "format", "path");
|
|
@@ -1279,7 +1613,8 @@ function recordAgentEvent(p) {
|
|
|
1279
1613
|
const hasExplicitRoot = !!opt(p, "artifact-root");
|
|
1280
1614
|
const root = explicitArtifactRoot(p);
|
|
1281
1615
|
const explicit = opt(p, "artifact-dir");
|
|
1282
|
-
const
|
|
1616
|
+
const actorKey = resolveReadActorKey(p);
|
|
1617
|
+
const dir = explicit ? path.resolve(explicit) : currentDir(root, actorKey);
|
|
1283
1618
|
if (!dir || !fs.existsSync(dir))
|
|
1284
1619
|
die("artifact directory does not exist");
|
|
1285
1620
|
if (explicit && fs.lstatSync(dir).isSymbolicLink())
|
|
@@ -1288,9 +1623,32 @@ function recordAgentEvent(p) {
|
|
|
1288
1623
|
requireArtifactDirUnderRoot(dir, root);
|
|
1289
1624
|
const timestamp = opt(p, "timestamp", now());
|
|
1290
1625
|
const agent = validateAgentId(opt(p, "agent-id"));
|
|
1291
|
-
|
|
1626
|
+
// #376 model routing: optionally stamp the delegate role/model resolved from
|
|
1627
|
+
// .datum/config.json onto the event so a downstream economics record (#349)
|
|
1628
|
+
// can price role assignments per delegation, and so an escalate-on-gate-failure
|
|
1629
|
+
// re-dispatch records which tier it climbed FROM. Fully additive/optional: when
|
|
1630
|
+
// no routing flag is passed the event shape is byte-identical to before.
|
|
1631
|
+
// These live as TOP-LEVEL event fields (not nested) on purpose: appendJsonl's
|
|
1632
|
+
// serializer (spacedLine) uses the top-level key list as a JSON.stringify array
|
|
1633
|
+
// replacer, which is an allowlist applied at every nesting level — a nested
|
|
1634
|
+
// routing object would have its inner keys stripped. Flat keeps the shape a
|
|
1635
|
+
// simple per-event routing record a JSONL economics feed can read directly.
|
|
1636
|
+
const role = opt(p, "role");
|
|
1637
|
+
const model = opt(p, "model");
|
|
1638
|
+
const escalatedFrom = opt(p, "escalated-from");
|
|
1639
|
+
const event = {
|
|
1640
|
+
timestamp,
|
|
1641
|
+
agent_id: agent,
|
|
1642
|
+
kind: opt(p, "kind", "note"),
|
|
1643
|
+
status: opt(p, "status", "info"),
|
|
1644
|
+
summary: opt(p, "summary"),
|
|
1645
|
+
...(opt(p, "ref") ? { ref: opt(p, "ref") } : {}),
|
|
1646
|
+
...(role ? { role } : {}),
|
|
1647
|
+
...(model ? { model } : {}),
|
|
1648
|
+
...(escalatedFrom ? { escalated_from: escalatedFrom } : {}),
|
|
1649
|
+
};
|
|
1292
1650
|
appendJsonl(path.join(dir, "agents", agent, "events.jsonl"), event);
|
|
1293
|
-
updateCurrentAgent(root, dir, agent, event.status, timestamp);
|
|
1651
|
+
updateCurrentAgent(root, dir, agent, event.status, timestamp, actorKey);
|
|
1294
1652
|
return 0;
|
|
1295
1653
|
}
|
|
1296
1654
|
function initPlan(p) {
|
|
@@ -1732,9 +2090,14 @@ async function recordGateClaim(p) {
|
|
|
1732
2090
|
die("--status must be one of: pass, fail, not_verified");
|
|
1733
2091
|
const summary = opt(p, "summary") || die("--summary is required");
|
|
1734
2092
|
const expectationId = opt(p, "expectation");
|
|
1735
|
-
// Resolve the active flow step from current.json
|
|
2093
|
+
// Resolve the active flow step from current.json. #291 Wave 2 Task 2.1 (§7)/Task 2.2: resolve
|
|
2094
|
+
// the CALLING actor's own current-pointer (per-actor-first, legacy-fallback) rather than an
|
|
2095
|
+
// unconditional legacy-only read — this is the fix for record-gate-claim's pre-existing (#291
|
|
2096
|
+
// Stop-short risk) lack of a dir-scoping guard: it now resolves ITS OWN actor's current.json
|
|
2097
|
+
// view, not a different actor's more-recently-written legacy pointer.
|
|
1736
2098
|
const flowAgentsDir = path.dirname(dir);
|
|
1737
|
-
const
|
|
2099
|
+
const gateClaimActorKey = resolveReadActorKey(p);
|
|
2100
|
+
const activeStep = resolveActiveFlowStep(flowAgentsDir, gateClaimActorKey);
|
|
1738
2101
|
if (!activeStep)
|
|
1739
2102
|
die("record-gate-claim requires an active flow step in current.json (set via ensure-session --flow-id or advance-state --flow-definition)");
|
|
1740
2103
|
const expects = activeStep.gateExpects;
|
|
@@ -1781,7 +2144,7 @@ async function recordGateClaim(p) {
|
|
|
1781
2144
|
checkNormalized._waiver = gateWaiver;
|
|
1782
2145
|
// Log the targeted gate expectation for transparency (goes to stderr only)
|
|
1783
2146
|
process.stderr.write(`[record-gate-claim] targeting ${activeStep.stepId}/${activeStep.gateId}/${targetExpectation.id} → claimType=${claimType} subjectType=${subjectType}${gateWaiver ? " (WAIVED accepted_gap)" : ""}\n`);
|
|
1784
|
-
assertBundleWritten(await writeTrustBundle(dir, slug, ts, [checkNormalized], [], []));
|
|
2147
|
+
assertBundleWritten(await writeTrustBundle(dir, slug, ts, [checkNormalized], [], [], gateClaimActorKey));
|
|
1785
2148
|
return 0;
|
|
1786
2149
|
}
|
|
1787
2150
|
/**
|
|
@@ -1917,7 +2280,11 @@ async function advanceState(p) {
|
|
|
1917
2280
|
const phaseMap = resolvePhaseMap(flow, repoRoot);
|
|
1918
2281
|
const stepId = phaseMap?.[phase] ?? undefined;
|
|
1919
2282
|
if (stepId) {
|
|
1920
|
-
|
|
2283
|
+
// #291 Wave 2 Task 2.1 (§5): thread the calling actor through so this second writeCurrent()
|
|
2284
|
+
// call site ALSO dual-writes the per-actor projection, not only ensure-session's call site —
|
|
2285
|
+
// otherwise a session that only ever calls advance-state (never re-running ensure-session)
|
|
2286
|
+
// would never get a per-actor current.json mirror for its own FlowDefinition routing keys.
|
|
2287
|
+
writeCurrent(root, dir, timestamp, "workflow-sidecar", "advance-state", flow, stepId, undefined, resolveReadActorKey(p));
|
|
1921
2288
|
}
|
|
1922
2289
|
}
|
|
1923
2290
|
livenessLifecycle(dir, slug, LIVENESS_TERMINAL.has(status) ? "release" : "heartbeat", timestamp);
|
|
@@ -2238,48 +2605,115 @@ async function sealCheckpoint(p) {
|
|
|
2238
2605
|
// Idempotent: overwrites on re-delivery.
|
|
2239
2606
|
// Called automatically from recordRelease and advanceState→delivered (best-effort).
|
|
2240
2607
|
// Also exposed as the `publish-delivery <artifact-dir>` subcommand for explicit use.
|
|
2608
|
+
/**
|
|
2609
|
+
* #379 supersede-on-publish cleanup: keep delivery/ bounded by pruning inherited PER-SESSION
|
|
2610
|
+
* seal dirs (the growth vector), scoped to avoid any cross-PR conflict.
|
|
2611
|
+
*
|
|
2612
|
+
* An inherited per-session seal dir (`delivery/<other-slug>/`) is UNIQUELY named, so pruning
|
|
2613
|
+
* it can never conflict with a concurrent PR: two branches deleting the same inherited dir is
|
|
2614
|
+
* a delete/delete (auto-merges), and each new delivery adds its OWN distinct
|
|
2615
|
+
* `delivery/<slug>/`. And it is HARMLESS to leave: trust-reconcile.js's prefer-newest
|
|
2616
|
+
* ownership selection always picks THIS session's fresher bundle over an older inherited one.
|
|
2617
|
+
* Pruning is therefore purely to stop unbounded accumulation of permanently-superseded dirs.
|
|
2618
|
+
*
|
|
2619
|
+
* The SHARED FLAT path (`delivery/trust.bundle` + checkpoints) is deliberately NOT pruned
|
|
2620
|
+
* here. During the migration window other concurrent PRs may still seal to the flat path;
|
|
2621
|
+
* deleting it would produce a modify/delete conflict → a DIRTY PR → the no-CI failure this
|
|
2622
|
+
* whole change fixes. The flat path is a single fixed location (not a growth vector) and the
|
|
2623
|
+
* reconciler treats a stale flat bundle as non-owning / older-owning, so leaving it is safe.
|
|
2624
|
+
* Removing the flat legacy seals is a one-time cleanup for a dedicated PR once no open PR
|
|
2625
|
+
* still seals to it — intentionally NOT bundled into every delivery.
|
|
2626
|
+
*
|
|
2627
|
+
* Best-effort: a prune failure is logged, never fatal to the delivery. Never touches
|
|
2628
|
+
* README.md, DECLARED, the flat seal files, or any subdir that is not itself a seal dir.
|
|
2629
|
+
*/
|
|
2630
|
+
function pruneSupersededSeals(deliveryDir, keepSlug) {
|
|
2631
|
+
let entries = [];
|
|
2632
|
+
try {
|
|
2633
|
+
entries = fs.readdirSync(deliveryDir, { withFileTypes: true });
|
|
2634
|
+
}
|
|
2635
|
+
catch {
|
|
2636
|
+
entries = [];
|
|
2637
|
+
}
|
|
2638
|
+
for (const entry of entries) {
|
|
2639
|
+
if (!entry.isDirectory() || entry.name === keepSlug)
|
|
2640
|
+
continue;
|
|
2641
|
+
const subdir = path.join(deliveryDir, entry.name);
|
|
2642
|
+
// Only prune dirs that actually look like a seal dir (contain a trust.bundle or
|
|
2643
|
+
// trust.checkpoint.json) — never an unrelated directory a human placed under delivery/.
|
|
2644
|
+
const looksLikeSeal = fs.existsSync(path.join(subdir, "trust.bundle")) ||
|
|
2645
|
+
fs.existsSync(path.join(subdir, "trust.checkpoint.json"));
|
|
2646
|
+
if (!looksLikeSeal)
|
|
2647
|
+
continue;
|
|
2648
|
+
try {
|
|
2649
|
+
fs.rmSync(subdir, { recursive: true, force: true });
|
|
2650
|
+
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`);
|
|
2651
|
+
}
|
|
2652
|
+
catch (err) {
|
|
2653
|
+
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`);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2241
2657
|
/**
|
|
2242
2658
|
* Publish the session's trust artifacts to the committed delivery/ path.
|
|
2243
2659
|
*
|
|
2660
|
+
* #379: writes to a PER-SESSION path `<repoRoot>/delivery/<slug>/` (slug = the session
|
|
2661
|
+
* artifact dir's basename) rather than the shared flat `delivery/`. A shared path guarantees
|
|
2662
|
+
* a git conflict between ANY two concurrent deliveries, and a conflicting (DIRTY) PR gets NO
|
|
2663
|
+
* pull_request workflows — the required Trust Reconcile check silently never runs (field
|
|
2664
|
+
* incidents #330/#358/#378). Per-session paths make concurrent deliveries write DISTINCT
|
|
2665
|
+
* files that never contend. trust-reconcile.js reads both the flat (back-compat) and
|
|
2666
|
+
* per-session layouts via resolveDeliveryCandidates() and selects the owning candidate by
|
|
2667
|
+
* commit ancestry.
|
|
2668
|
+
*
|
|
2244
2669
|
* Copies trust.bundle, trust.checkpoint.json, and (if present)
|
|
2245
|
-
* trust.checkpoint.intoto.json / trust.checkpoint.sig.json
|
|
2246
|
-
* session artifact dir to <repoRoot>/delivery
|
|
2670
|
+
* trust.checkpoint.intoto.json / trust.checkpoint.sig.json / trust.checkpoint.attestation.json
|
|
2671
|
+
* from the session artifact dir to <repoRoot>/delivery/<slug>/.
|
|
2247
2672
|
*
|
|
2248
2673
|
* Fail-soft on a missing bundle: if trust.bundle is absent, returns without throwing.
|
|
2249
2674
|
* Fail-CLOSED on repo-root resolution: repoRoot must be a real, resolved kits/ ancestor
|
|
2250
2675
|
* (see findRepoRootFromDirStrict) — null (no ancestor found) skips the publish with a
|
|
2251
2676
|
* visible warning instead of writing to whatever process.cwd() happens to be. This
|
|
2252
2677
|
* prevents a scratch/test session dir (no kits/ ancestor) from silently clobbering an
|
|
2253
|
-
* unrelated real repo's delivery/
|
|
2678
|
+
* unrelated real repo's delivery/ seal when invoked with that repo as cwd (see
|
|
2254
2679
|
* evals/integration/test_checkpoint_signing.sh TEST 2 and the WS5 session findings at
|
|
2255
2680
|
* .kontourai/flow-agents/ws5-governance-kit-slice1 for the root cause this fixes).
|
|
2256
|
-
* Idempotent: overwrites on re-delivery.
|
|
2681
|
+
* Idempotent: overwrites on re-delivery to the same slug.
|
|
2257
2682
|
*/
|
|
2258
2683
|
export async function publishDelivery(dir, repoRoot) {
|
|
2259
2684
|
const bundleSrc = path.join(dir, "trust.bundle");
|
|
2260
2685
|
if (!fs.existsSync(bundleSrc))
|
|
2261
2686
|
return; // no bundle — skip gracefully
|
|
2262
2687
|
if (!repoRoot) {
|
|
2263
|
-
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/
|
|
2688
|
+
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`);
|
|
2264
2689
|
return;
|
|
2265
2690
|
}
|
|
2266
2691
|
const deliveryDir = path.join(repoRoot, "delivery");
|
|
2692
|
+
// #379: slug is the session artifact dir's basename — the same human-meaningful id used
|
|
2693
|
+
// throughout the session (.kontourai/flow-agents/<slug>/). The per-session dir NAME is only
|
|
2694
|
+
// a collision-avoidance handle; ownership is decided by commit ancestry, not by name.
|
|
2695
|
+
const slug = path.basename(path.resolve(dir));
|
|
2696
|
+
const sessionDeliveryDir = path.join(deliveryDir, slug);
|
|
2267
2697
|
fs.mkdirSync(deliveryDir, { recursive: true });
|
|
2698
|
+
// Supersede inherited/flat seals BEFORE writing this session's dir (keepSlug = our own).
|
|
2699
|
+
pruneSupersededSeals(deliveryDir, slug);
|
|
2700
|
+
fs.mkdirSync(sessionDeliveryDir, { recursive: true });
|
|
2268
2701
|
// Required: trust.bundle (the CI anchor)
|
|
2269
|
-
fs.copyFileSync(bundleSrc, path.join(
|
|
2702
|
+
fs.copyFileSync(bundleSrc, path.join(sessionDeliveryDir, "trust.bundle"));
|
|
2270
2703
|
// Optional companions: checkpoint + signing artifacts
|
|
2271
2704
|
const companions = [
|
|
2272
2705
|
"trust.checkpoint.json",
|
|
2273
2706
|
"trust.checkpoint.intoto.json",
|
|
2274
2707
|
"trust.checkpoint.sig.json",
|
|
2708
|
+
"trust.checkpoint.attestation.json",
|
|
2275
2709
|
];
|
|
2276
2710
|
for (const filename of companions) {
|
|
2277
2711
|
const src = path.join(dir, filename);
|
|
2278
2712
|
if (fs.existsSync(src)) {
|
|
2279
|
-
fs.copyFileSync(src, path.join(
|
|
2713
|
+
fs.copyFileSync(src, path.join(sessionDeliveryDir, filename));
|
|
2280
2714
|
}
|
|
2281
2715
|
}
|
|
2282
|
-
process.stderr.write(`[publish-delivery] published trust.bundle and companions to ${
|
|
2716
|
+
process.stderr.write(`[publish-delivery] published trust.bundle and companions to ${sessionDeliveryDir} (per-session path, #379)\n`);
|
|
2283
2717
|
}
|
|
2284
2718
|
/**
|
|
2285
2719
|
* publish-delivery <artifact-dir> [--repo-root <path>]
|
|
@@ -3008,11 +3442,24 @@ function livenessLabel(status) {
|
|
|
3008
3442
|
return "superseded";
|
|
3009
3443
|
return status;
|
|
3010
3444
|
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Delegate to the shared pure-CJS liveness reader's freshHolders() (scripts/hooks/lib/liveness-read.js),
|
|
3447
|
+
* mirroring the exact loader shape assignment-provider.ts already uses for the same helper (#291
|
|
3448
|
+
* Wave 2 Task 2.1) — so ensure-session's ownership guard computes freshness identically to
|
|
3449
|
+
* `assignment-provider status`'s own join, a single implementation. `readLivenessEvents` above
|
|
3450
|
+
* already loads this same module inline for its own narrower need (just the events reader); this
|
|
3451
|
+
* loader additionally exposes `freshHolders` for the guard's `computeEffectiveState` call.
|
|
3452
|
+
*/
|
|
3453
|
+
function loadLivenessReadHelper() {
|
|
3454
|
+
const _req = createRequire(import.meta.url);
|
|
3455
|
+
const helperPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../scripts/hooks/lib/liveness-read.js");
|
|
3456
|
+
return _req(helperPath);
|
|
3457
|
+
}
|
|
3011
3458
|
// ─── ADR 0012 lifecycle-driven liveness (default-on; opt-out via FLOW_AGENTS_LIVENESS) ──
|
|
3012
3459
|
// init-plan claims the work-item; advance-state heartbeats (or releases on terminal),
|
|
3013
3460
|
// so the workflow lifecycle itself maintains the liveness claim — no manual liveness calls.
|
|
3014
3461
|
// Additive + fail-open: a liveness-emit failure never affects the workflow command.
|
|
3015
|
-
const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
3462
|
+
export const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
|
|
3016
3463
|
/**
|
|
3017
3464
|
* Delegate to the shared pure-CJS resolver (scripts/hooks/lib/actor-identity.js), mirroring the
|
|
3018
3465
|
* createRequire pattern used by readLivenessEvents() above. Deliberately NO inline duplicate
|