@kontourai/flow-agents 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.github/workflows/ci.yml +4 -0
  2. package/CHANGELOG.md +8 -0
  3. package/build/src/builder-flow-run-adapter.d.ts +10 -1
  4. package/build/src/builder-flow-run-adapter.js +29 -1
  5. package/build/src/builder-flow-runtime.d.ts +18 -0
  6. package/build/src/builder-flow-runtime.js +205 -13
  7. package/build/src/builder-lifecycle-authority.d.ts +35 -0
  8. package/build/src/builder-lifecycle-authority.js +219 -0
  9. package/build/src/cli/assignment-provider.d.ts +10 -0
  10. package/build/src/cli/assignment-provider.js +61 -52
  11. package/build/src/cli/builder-run.js +46 -5
  12. package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
  13. package/build/src/cli/workflow-sidecar.d.ts +3 -0
  14. package/build/src/cli/workflow-sidecar.js +28 -6
  15. package/build/src/cli/workflow.d.ts +2 -0
  16. package/build/src/cli/workflow.js +521 -0
  17. package/build/src/cli.js +2 -0
  18. package/build/src/index.d.ts +4 -0
  19. package/build/src/index.js +2 -0
  20. package/build/src/lib/package-version.d.ts +2 -0
  21. package/build/src/lib/package-version.js +13 -0
  22. package/build/src/lib/pinned-cli-command.d.ts +6 -0
  23. package/build/src/lib/pinned-cli-command.js +21 -0
  24. package/context/contracts/artifact-contract.md +1 -1
  25. package/context/scripts/hooks/config-protection.js +8 -1
  26. package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
  27. package/context/scripts/hooks/stop-goal-fit.js +1 -1
  28. package/docs/context-map.md +2 -0
  29. package/docs/public-workflow-cli.md +63 -0
  30. package/docs/spec/builder-flow-runtime.md +37 -0
  31. package/docs/workflow-usage-guide.md +5 -0
  32. package/evals/ci/run-baseline.sh +2 -0
  33. package/evals/integration/test_builder_entry_enforcement.sh +5 -4
  34. package/evals/integration/test_bundle_install.sh +59 -5
  35. package/evals/integration/test_public_workflow_cli.sh +259 -0
  36. package/package.json +2 -2
  37. package/schemas/builder-lifecycle-authorization.schema.json +57 -0
  38. package/schemas/lifecycle-authority-keys.schema.json +25 -0
  39. package/schemas/workflow-state.schema.json +1 -1
  40. package/scripts/hooks/config-protection.js +8 -1
  41. package/scripts/hooks/lib/config-protection-remedies.js +3 -0
  42. package/scripts/hooks/stop-goal-fit.js +1 -1
  43. package/src/builder-flow-run-adapter.ts +47 -0
  44. package/src/builder-flow-runtime.ts +216 -4
  45. package/src/builder-lifecycle-authority.ts +218 -0
  46. package/src/cli/assignment-provider.ts +29 -9
  47. package/src/cli/builder-flow-runtime.test.mjs +404 -1
  48. package/src/cli/builder-run.ts +56 -5
  49. package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
  50. package/src/cli/workflow-sidecar.ts +28 -6
  51. package/src/cli/workflow.ts +471 -0
  52. package/src/cli.ts +2 -0
  53. package/src/index.ts +14 -0
  54. package/src/lib/package-version.ts +15 -0
  55. package/src/lib/pinned-cli-command.ts +23 -0
@@ -54,6 +54,10 @@ export type AssignmentStatus = {
54
54
  record: AssignmentClaimRecord | null;
55
55
  has_claim_label?: boolean;
56
56
  };
57
+ export declare function resolveCurrentAssignmentActor(): {
58
+ actor: ActorStruct;
59
+ actorKey: string;
60
+ };
57
61
  export declare function assignmentFilePath(artifactRoot: string, subjectId: string): string;
58
62
  export declare function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null;
59
63
  export declare function writeLocalRecord(artifactRoot: string, subjectId: string, record: AssignmentClaimRecord): void;
@@ -164,6 +168,12 @@ export declare function performLocalRelease(artifactRoot: string, subjectId: str
164
168
  actorKey?: string;
165
169
  tolerateNoActiveClaim?: boolean;
166
170
  }): AssignmentClaimRecord | null;
171
+ /** Caller must already hold this subject's assignment lock through withSubjectLock(). */
172
+ export declare function performLocalReleaseUnderLock(artifactRoot: string, subjectId: string, releasedBy: ActorStruct | null, opts?: {
173
+ reason?: string;
174
+ actorKey?: string;
175
+ tolerateNoActiveClaim?: boolean;
176
+ }): AssignmentClaimRecord | null;
167
177
  /**
168
178
  * Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
169
179
  * withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
@@ -76,6 +76,9 @@ function loadActorStruct(args) {
76
76
  const actorJsonPath = flagString(args.flags, "actor-json");
77
77
  if (actorJsonPath)
78
78
  return { actor: loadActorStructFromFile(actorJsonPath) };
79
+ return resolveCurrentAssignmentActor();
80
+ }
81
+ export function resolveCurrentAssignmentActor() {
79
82
  const helper = loadActorIdentityHelper();
80
83
  const resolved = helper.resolveActor(process.env);
81
84
  if (helper.isUnresolvedActor(resolved.actor))
@@ -86,9 +89,15 @@ function loadActorStruct(args) {
86
89
  // CI session — actor_key stays correct (so no false-block), but record.actor is malformed and the
87
90
  // audit-trail / `assignment-provider status` output for CI sessions would be corrupt.
88
91
  const ci = resolved.source.startsWith("ci-runtime") ? helper.detectCiActor(process.env) : null;
89
- const actor = ci && ci.session_id
90
- ? { runtime: ci.runtime, session_id: ci.session_id, host: os.hostname(), human: null }
91
- : { runtime: helper.detectRuntime(process.env), session_id: resolved.actor, host: os.hostname(), human: null };
92
+ const runtimeSessionId = resolved.source.startsWith("runtime-session-id") ? helper.runtimeSessionId(process.env) : "";
93
+ const ancestrySeed = resolved.source === "process-ancestry" ? helper.ancestorActorSeed() : "";
94
+ const actor = resolved.source === "explicit-override"
95
+ ? { runtime: "explicit-override", session_id: resolved.actor, host: os.hostname(), human: null }
96
+ : ci && ci.session_id
97
+ ? { runtime: ci.runtime, session_id: ci.session_id, host: os.hostname(), human: null }
98
+ : runtimeSessionId
99
+ ? { runtime: helper.detectRuntime(process.env), session_id: runtimeSessionId, host: os.hostname(), human: null }
100
+ : { runtime: helper.detectRuntime(process.env), session_id: ancestrySeed ? `anc-${ancestrySeed}` : resolved.actor, host: os.hostname(), human: null };
92
101
  return { actor, actorKey: resolved.actor };
93
102
  }
94
103
  export function assignmentFilePath(artifactRoot, subjectId) {
@@ -522,61 +531,61 @@ function claimLocalFile(argv) {
522
531
  * than allowed to silently no-op or wrongly refuse.
523
532
  */
524
533
  export function performLocalRelease(artifactRoot, subjectId, releasedBy, opts = {}) {
534
+ return withSubjectLock(artifactRoot, subjectId, () => performLocalReleaseUnderLock(artifactRoot, subjectId, releasedBy, opts));
535
+ }
536
+ /** Caller must already hold this subject's assignment lock through withSubjectLock(). */
537
+ export function performLocalReleaseUnderLock(artifactRoot, subjectId, releasedBy, opts = {}) {
525
538
  const helper = loadActorIdentityHelper();
526
539
  const reason = opts.reason ?? "released";
527
540
  const tolerateNoActiveClaim = opts.tolerateNoActiveClaim ?? false;
528
- // F1 fix (fix-plan iteration 1, CRITICAL): release mutates the same record file claim/supersede
529
- // do, under the same per-subject lock (see withSubjectLock()'s doc comment).
530
- return withSubjectLock(artifactRoot, subjectId, () => {
531
- const existing = readLocalRecord(artifactRoot, subjectId);
532
- if (!existing || existing.status !== "claimed") {
533
- if (tolerateNoActiveClaim)
541
+ const existing = readLocalRecord(artifactRoot, subjectId);
542
+ if (!existing || existing.status !== "claimed") {
543
+ if (tolerateNoActiveClaim)
544
+ return null;
545
+ throw new Error(`no active claim to release for subject: ${subjectId}`);
546
+ }
547
+ if (releasedBy) {
548
+ // Contract guard (hardening fix, #292 review): a caller that supplies `releasedBy` but NOT
549
+ // `opts.actorKey` against a record that already carries `actor_key` cannot reliably prove
550
+ // ownership — see this function's doc comment. This is the ONLY combination that fires: it
551
+ // does NOT fire when `existing.actor_key` is absent (the CLI/fixture path, where both sides
552
+ // fall back to serializeActor() and legitimately compare equal). Fail loudly rather than
553
+ // silently no-op (tolerant callers) or wrongly refuse (throwing callers) — never silent.
554
+ if (!opts.actorKey && existing.actor_key) {
555
+ if (tolerateNoActiveClaim) {
556
+ console.error(`[performLocalRelease] cannot verify ownership of an actor_key-stamped record without opts.actorKey; skipping release for ${subjectId}`);
534
557
  return null;
535
- throw new Error(`no active claim to release for subject: ${subjectId}`);
536
- }
537
- if (releasedBy) {
538
- // Contract guard (hardening fix, #292 review): a caller that supplies `releasedBy` but NOT
539
- // `opts.actorKey` against a record that already carries `actor_key` cannot reliably prove
540
- // ownership — see this function's doc comment. This is the ONLY combination that fires: it
541
- // does NOT fire when `existing.actor_key` is absent (the CLI/fixture path, where both sides
542
- // fall back to serializeActor() and legitimately compare equal). Fail loudly rather than
543
- // silently no-op (tolerant callers) or wrongly refuse (throwing callers) — never silent.
544
- if (!opts.actorKey && existing.actor_key) {
545
- if (tolerateNoActiveClaim) {
546
- console.error(`[performLocalRelease] cannot verify ownership of an actor_key-stamped record without opts.actorKey; skipping release for ${subjectId}`);
547
- return null;
548
- }
549
- throw new Error("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");
550
- }
551
- // AC6: never force-release a claim held by a different actor. Mirrors
552
- // computeEffectiveState()'s canonical self-recognition comparison EXACTLY —
553
- // `holderActorKey` prefers the stored `actor_key` (the canonical resolveActor(env).actor
554
- // string, present on records written by the fixed performLocalClaim/performLocalSupersede
555
- // paths) and only falls back to `serializeActor(existing.actor)` when `actor_key` is
556
- // absent (every pre-fix record, every #290 eval fixture). The releaser's side must use the
557
- // SAME canonical form: `opts.actorKey` (the caller's resolveActor(env).actor string, e.g.
558
- // scripts/hooks/stop-goal-fit.js's Stop hook) when provided, else re-derived via
559
- // serializeActor(releasedBy) — never serializeActor() unconditionally on both sides, which
560
- // would compare the bare actor_key form against a re-derived triple form for an
561
- // explicit-override actor and spuriously reject a legitimate same-actor release (the #291
562
- // seam, relocated to this write path).
563
- const holderActorKey = existing.actor_key || helper.serializeActor(existing.actor);
564
- const releasedByActorKey = opts.actorKey || helper.serializeActor(releasedBy);
565
- if (holderActorKey !== releasedByActorKey) {
566
- if (tolerateNoActiveClaim)
567
- return null;
568
- throw new Error(`--actor-json does not match the current holder (${holderActorKey}); refusing to release a claim held by someone else`);
569
558
  }
559
+ throw new Error("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");
570
560
  }
571
- const record = {
572
- ...existing,
573
- ...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
574
- status: "released",
575
- audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "release", from_actor: existing.actor, to_actor: releasedBy, reason }],
576
- };
577
- writeLocalRecord(artifactRoot, subjectId, record);
578
- return record;
579
- });
561
+ // AC6: never force-release a claim held by a different actor. Mirrors
562
+ // computeEffectiveState()'s canonical self-recognition comparison EXACTLY —
563
+ // `holderActorKey` prefers the stored `actor_key` (the canonical resolveActor(env).actor
564
+ // string, present on records written by the fixed performLocalClaim/performLocalSupersede
565
+ // paths) and only falls back to `serializeActor(existing.actor)` when `actor_key` is
566
+ // absent (every pre-fix record, every #290 eval fixture). The releaser's side must use the
567
+ // SAME canonical form: `opts.actorKey` (the caller's resolveActor(env).actor string, e.g.
568
+ // scripts/hooks/stop-goal-fit.js's Stop hook) when provided, else re-derived via
569
+ // serializeActor(releasedBy) — never serializeActor() unconditionally on both sides, which
570
+ // would compare the bare actor_key form against a re-derived triple form for an
571
+ // explicit-override actor and spuriously reject a legitimate same-actor release (the #291
572
+ // seam, relocated to this write path).
573
+ const holderActorKey = existing.actor_key || helper.serializeActor(existing.actor);
574
+ const releasedByActorKey = opts.actorKey || helper.serializeActor(releasedBy);
575
+ if (holderActorKey !== releasedByActorKey) {
576
+ if (tolerateNoActiveClaim)
577
+ return null;
578
+ throw new Error(`--actor-json does not match the current holder (${holderActorKey}); refusing to release a claim held by someone else`);
579
+ }
580
+ }
581
+ const record = {
582
+ ...existing,
583
+ ...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
584
+ status: "released",
585
+ audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "release", from_actor: existing.actor, to_actor: releasedBy, reason }],
586
+ };
587
+ writeLocalRecord(artifactRoot, subjectId, record);
588
+ return record;
580
589
  }
581
590
  function releaseLocalFile(argv) {
582
591
  const args = parseArgs(argv);
@@ -1,35 +1,76 @@
1
1
  import { flagString, parseArgs } from "../lib/args.js";
2
- import { recoverBuilderFlowSession, startBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
2
+ import { cancelBuilderFlowSession, archiveBuilderFlowSession, pauseBuilderFlowSession, recoverBuilderFlowSession, releaseBuilderFlowAssignment, resumeBuilderFlowSession, startBuilderFlowSession, syncBuilderFlowSession, } from "../builder-flow-runtime.js";
3
+ const USAGE = "Usage: flow-agents builder-run <start|sync|recover|pause|resume|cancel|release-assignment|archive> --session-dir <path> [--reason <text> | --authorization-file <path>]";
3
4
  export async function main(argv) {
4
5
  const parsed = parseArgs(argv);
5
6
  const action = parsed.positionals[0];
6
7
  const sessionDir = flagString(parsed.flags, "session-dir");
8
+ const authorizationFile = flagString(parsed.flags, "authorization-file");
9
+ const reason = flagString(parsed.flags, "reason");
7
10
  const validRecoveryArguments = parsed.positionals.length === 1
8
11
  && Object.keys(parsed.flags).length === 1
9
12
  && typeof parsed.flags["session-dir"] === "string";
10
13
  if (action === "recover" && !validRecoveryArguments) {
11
- console.error("Usage: flow-agents builder-run <start|sync|recover> --session-dir <path>");
14
+ console.error(USAGE);
12
15
  return 64;
13
16
  }
14
17
  if (!sessionDir) {
15
18
  console.error("builder-run requires --session-dir .kontourai/flow-agents/<slug>");
16
19
  return 64;
17
20
  }
18
- if (action !== "start" && action !== "sync" && action !== "recover") {
19
- console.error("Usage: flow-agents builder-run <start|sync|recover> --session-dir <path>");
21
+ if (!action || !["start", "sync", "recover", "pause", "resume", "cancel", "release-assignment", "archive"].includes(action)) {
22
+ console.error(USAGE);
23
+ return 64;
24
+ }
25
+ const agentLifecycle = action === "pause" || action === "resume" || action === "release-assignment";
26
+ const authorizedLifecycle = action === "cancel" || action === "archive";
27
+ const lifecycle = agentLifecycle || authorizedLifecycle;
28
+ const allowedLifecycleFlag = (name) => name === "session-dir" || (agentLifecycle ? name === "reason" : name === "authorization-file");
29
+ if (lifecycle && (parsed.positionals.length !== 1 || Object.keys(parsed.flags).some((name) => !allowedLifecycleFlag(name)))) {
30
+ console.error(USAGE);
31
+ return 64;
32
+ }
33
+ if (agentLifecycle && !reason) {
34
+ console.error(`builder-run ${action} requires --reason <text>`);
35
+ return 64;
36
+ }
37
+ if (authorizedLifecycle && !authorizationFile) {
38
+ console.error(`builder-run ${action} requires a signed --authorization-file <path>`);
39
+ return 64;
40
+ }
41
+ if (!lifecycle && (authorizationFile || reason)) {
42
+ console.error(USAGE);
20
43
  return 64;
21
44
  }
22
45
  const result = action === "start"
23
46
  ? await startBuilderFlowSession({ sessionDir })
24
47
  : action === "sync"
25
48
  ? await syncBuilderFlowSession({ sessionDir })
26
- : await recoverBuilderFlowSession({ sessionDir });
49
+ : action === "recover"
50
+ ? await recoverBuilderFlowSession({ sessionDir })
51
+ : action === "pause"
52
+ ? await pauseBuilderFlowSession({ sessionDir, reason: reason })
53
+ : action === "resume"
54
+ ? await resumeBuilderFlowSession({ sessionDir, reason: reason })
55
+ : action === "cancel"
56
+ ? await cancelBuilderFlowSession({ sessionDir, authorizationFile: authorizationFile })
57
+ : action === "release-assignment"
58
+ ? await releaseBuilderFlowAssignment({ sessionDir, reason: reason })
59
+ : await archiveBuilderFlowSession({ sessionDir, authorizationFile: authorizationFile });
27
60
  console.log(JSON.stringify({
28
61
  run_id: result.run.runId,
29
62
  definition_id: result.run.definitionId,
30
63
  current_step: result.run.state.current_step,
31
64
  status: result.run.state.status,
32
65
  attached: result.attached,
66
+ ...(action === "cancel" ? {
67
+ assignment_released: "assignmentReleased" in result ? result.assignmentReleased : false,
68
+ idempotent: "idempotent" in result ? result.idempotent : false,
69
+ } : action === "release-assignment" ? {
70
+ assignment_released: "assignmentReleased" in result ? result.assignmentReleased : false,
71
+ } : action === "archive" ? {
72
+ archive_dir: "archiveDir" in result ? result.archiveDir : null,
73
+ } : {}),
33
74
  next_action: result.projection.next_action,
34
75
  }));
35
76
  return 0;
@@ -255,6 +255,9 @@ function classifyWorkflow(slug, workflowPath) {
255
255
  if (status === "verified" && nextStatus === "done") {
256
256
  return { ...base, classification: "cleanup_candidate", reasons: ["verified workflow has next_action.status done"] };
257
257
  }
258
+ if (status === "canceled" && phase === "done") {
259
+ return { ...base, classification: "terminal_done", reasons: ["canceled workflow retains its artifacts without requiring delivery promotion"] };
260
+ }
258
261
  if (["delivered", "accepted", "archived"].includes(status) && phase === "done") {
259
262
  if (status !== "archived" && !hasPromotionClaim(workflowPath)) {
260
263
  return { ...base, classification: "cleanup_candidate", reasons: [`${status} workflow reached phase done without a promotion claim; ${PROMOTE_REMEDY}`] };
@@ -6,6 +6,7 @@ export declare const phases: string[];
6
6
  export declare const checkKinds: Set<string>;
7
7
  export declare const checkStatuses: Set<string>;
8
8
  export declare const verdicts: Set<string>;
9
+ export declare const WORKFLOW_WRITER_CONTRACT_VERSION = "1.0";
9
10
  export declare function writeJson(file: string, payload: AnyObj): void;
10
11
  export declare function loadJson(file: string, fallback?: AnyObj): AnyObj;
11
12
  export declare function appendJsonl(file: string, payload: AnyObj): void;
@@ -195,6 +196,7 @@ export declare function writeTrustBundle(dir: string, slug: string, timestamp: s
195
196
  errors: string[];
196
197
  }>;
197
198
  export declare function sidecarBase(slug: string): AnyObj;
199
+ export declare function currentWorkflowSessionDir(root: string): string | null;
198
200
  export declare function validateEvidenceRef(ref: AnyObj, label: string): AnyObj;
199
201
  export declare function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[];
200
202
  export declare function normalizeCheck(raw: AnyObj, allowGateClaimPrefix?: boolean, existingCheckStampById?: ReadonlyMap<string, boolean>): AnyObj;
@@ -622,3 +624,4 @@ export declare function buildGateInquiryRecords(bundle: BundleFile, blockSignal:
622
624
  export declare const LIVENESS_TERMINAL: Set<string>;
623
625
  export { buildClaimExplanation } from "./sidecar-claim-explain.js";
624
626
  export type { ClaimEvidenceItem, ClaimExplanation } from "./sidecar-claim-explain.js";
627
+ export declare function main(argv?: string[]): Promise<number>;
@@ -10,17 +10,20 @@ import { fileURLToPath } from "node:url";
10
10
  import { resolveActiveFlowStep, resolveAllFlowGateExpects, resolveFlowFilePath, resolvePhaseMap, resolveRouteBackPolicy } from "../lib/flow-resolver.js";
11
11
  import { defaultArtifactRootForRead, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
12
12
  import { ensureSafeDirectory } from "../lib/fs.js";
13
+ import { flowAgentsPackageVersion } from "../lib/package-version.js";
14
+ import { pinnedFlowAgentsCommand } from "../lib/pinned-cli-command.js";
13
15
  import { startBuilderFlowSession, syncBuilderFlowSessionIfPresent } from "../builder-flow-runtime.js";
14
16
  // #291 Wave 1 Task 1.1 exports: ensure-session's ownership guard reuses the EXACT same
15
17
  // assignment ⋈ liveness join / claim / supersede logic #290 already ships for the
16
18
  // `assignment-provider` CLI, rather than reimplementing a second, parallel join (static ESM
17
19
  // import — same idiom already used above for ../lib/flow-resolver.js).
18
20
  import { assignmentFilePath, computeEffectiveState, performLocalClaim, performLocalSupersede, readLocalAssignmentStatus, withSubjectLock } from "./assignment-provider.js";
19
- export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
21
+ export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "canceled", "accepted", "archived"]);
20
22
  export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
21
23
  export const checkKinds = new Set(["build", "types", "lint", "test", "command", "security", "diff", "browser", "runtime", "policy", "external"]);
22
24
  export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
23
25
  export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
26
+ export const WORKFLOW_WRITER_CONTRACT_VERSION = "1.0";
24
27
  function now() { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
25
28
  function read(file) { return fs.readFileSync(file, "utf8"); }
26
29
  export function writeJson(file, payload) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
@@ -1681,6 +1684,10 @@ function currentDir(root, actorKey) {
1681
1684
  }
1682
1685
  return dir;
1683
1686
  }
1687
+ export function currentWorkflowSessionDir(root) {
1688
+ const resolved = loadActorIdentityHelper().resolveActor(process.env);
1689
+ return currentDir(path.resolve(root), loadActorIdentityHelper().isUnresolvedActor(resolved.actor) ? undefined : resolved.actor);
1690
+ }
1684
1691
  /**
1685
1692
  * #291 Wave 2 Task 2.1 (§6): updates BOTH the legacy current.json (when IT points at `dir` — the
1686
1693
  * exact, unchanged existing check/write) AND the resolved actor's own per-actor current.json
@@ -2082,13 +2089,25 @@ async function ensureSession(p) {
2082
2089
  // takeover continuity true by construction (see Design Decision 3 in the plan).
2083
2090
  const branch = resolveSessionBranch(p, slug);
2084
2091
  const initialMarkdownStatus = entry.flowId ? "new" : "planning";
2085
- md = `# ${opt(p, "title", slug)}\n\nbranch: ${branch}\nworktree: main\ncreated: ${timestamp}\nstatus: ${initialMarkdownStatus}\ntype: deliver\niteration: 1\n\n## Plan\n\n${opt(p, "summary", "")}\n\n## Definition Of Done\n\n- **User outcome:** ${opt(p, "summary", "Workflow session is durable.")}\n- **Scope:** Workflow session artifacts and sidecars.\n- **Acceptance criteria:**\n${opts(p, "criterion").map((c) => ` - [ ] ${c} - Evidence: pending.`).join("\n")}\n- **Usefulness checks:**\n - [ ] User-facing workflow is documented or discoverable\n- **Stop-short risks:** Workflow artifacts could drift.\n- **Durable docs target:** not needed\n- **Sandbox mode:** local-edit\n\n## Execution Progress\n\n- [ ] Session initialized.\n\n## Verification Report\n\nBuild: [NOT_VERIFIED] Verification has not run yet.\n\n### Acceptance Criteria\n- [NOT_VERIFIED] Verification has not run yet - Evidence: pending workflow execution and checks.\n\n### Verdict: NOT_VERIFIED\n\n## Goal Fit Gate\n\n- [ ] Original user goal restated\n\n## Final Acceptance\n\n- [ ] CI/relevant checks passed or local equivalent recorded\n`;
2092
+ const acceptanceCriteria = opts(p, "criterion");
2093
+ if (acceptanceCriteria.length === 0)
2094
+ acceptanceCriteria.push(`Complete the requested outcome: ${opt(p, "summary", "Workflow session is durable.")}`);
2095
+ md = `# ${opt(p, "title", slug)}\n\nbranch: ${branch}\nworktree: main\ncreated: ${timestamp}\nstatus: ${initialMarkdownStatus}\ntype: deliver\niteration: 1\n\n## Plan\n\n${opt(p, "summary", "")}\n\n## Definition Of Done\n\n- **User outcome:** ${opt(p, "summary", "Workflow session is durable.")}\n- **Scope:** Workflow session artifacts and sidecars.\n- **Acceptance criteria:**\n${acceptanceCriteria.map((c) => ` - [ ] ${c} - Evidence: pending.`).join("\n")}\n- **Usefulness checks:**\n - [ ] User-facing workflow is documented or discoverable\n- **Stop-short risks:** Workflow artifacts could drift.\n- **Durable docs target:** not needed\n- **Sandbox mode:** local-edit\n\n## Execution Progress\n\n- [ ] Session initialized.\n\n## Verification Report\n\nBuild: [NOT_VERIFIED] Verification has not run yet.\n\n### Acceptance Criteria\n- [NOT_VERIFIED] Verification has not run yet - Evidence: pending workflow execution and checks.\n\n### Verdict: NOT_VERIFIED\n\n## Goal Fit Gate\n\n- [ ] Original user goal restated\n\n## Final Acceptance\n\n- [ ] CI/relevant checks passed or local equivalent recorded\n`;
2086
2096
  fs.writeFileSync(path.join(dir, `${slug}--deliver.md`), md);
2087
2097
  }
2088
2098
  if (!fs.existsSync(path.join(dir, "state.json")) || !fs.existsSync(path.join(dir, "acceptance.json")) || !fs.existsSync(path.join(dir, "handoff.json"))) {
2089
2099
  const phaseMap = entry.flowId ? resolvePhaseMap(entry.flowId, findRepoRootFromDir(dir)) : null;
2090
2100
  const initialPhase = Object.entries(phaseMap ?? {}).find(([, step]) => step === entry.firstStep)?.[0];
2091
- const startCommand = `flow-agents builder-run start --session-dir .kontourai/flow-agents/${slug}`;
2101
+ const startCommand = pinnedFlowAgentsCommand(flowAgentsPackageVersion(), [
2102
+ "workflow", "start",
2103
+ "--flow", "builder.build",
2104
+ "--work-item", workItem.ref,
2105
+ ...(workItem.ref === `local:${slug}` ? ["--task-slug", slug] : []),
2106
+ "--artifact-root", root,
2107
+ "--title", opt(p, "title", slug),
2108
+ "--summary", opt(p, "summary", "Workflow session is durable."),
2109
+ ...opts(p, "criterion").flatMap((criterion) => ["--criterion", criterion]),
2110
+ ]);
2092
2111
  const nextAction = entry.flowId
2093
2112
  ? entry.flowId === "builder.build"
2094
2113
  ? {
@@ -5396,7 +5415,7 @@ function loadLivenessReadHelper() {
5396
5415
  // init-plan claims the work-item; advance-state heartbeats (or releases on terminal),
5397
5416
  // so the workflow lifecycle itself maintains the liveness claim — no manual liveness calls.
5398
5417
  // Additive + fail-open: a liveness-emit failure never affects the workflow command.
5399
- export const LIVENESS_TERMINAL = new Set(["delivered", "accepted", "archived"]);
5418
+ export const LIVENESS_TERMINAL = new Set(["delivered", "canceled", "accepted", "archived"]);
5400
5419
  /**
5401
5420
  * Delegate to the shared pure-CJS resolver (scripts/hooks/lib/actor-identity.js), mirroring the
5402
5421
  * createRequire pattern used by readLivenessEvents() above. Deliberately NO inline duplicate
@@ -5840,8 +5859,8 @@ Available claim ids:
5840
5859
  return 0;
5841
5860
  }
5842
5861
  // ─────────────────────────────────────────────────────────────────────────────
5843
- async function main() {
5844
- const _rawArgv = process.argv.slice(2);
5862
+ export async function main(argv = process.argv.slice(2)) {
5863
+ const _rawArgv = argv;
5845
5864
  // #380: `record-check <dir> -- <command...>` — argv after the FIRST literal `--` token is the
5846
5865
  // command to execute verbatim (never option-parsed: a command like `npm test -- --watch`
5847
5866
  // legitimately contains its OWN `--`, so only the record-check dispatcher's own separator, the
@@ -5927,5 +5946,8 @@ catch {
5927
5946
  return process.argv[1];
5928
5947
  } })();
5929
5948
  if (_selfRealPath === _argv1RealPath) {
5949
+ if (path.basename(process.argv[1] ?? "") === "flow-agents-workflow-sidecar") {
5950
+ process.stderr.write("flow-agents-workflow-sidecar is deprecated; use `flow-agents workflow` or an explicitly pinned `npx @kontourai/flow-agents@<version> workflow` command.\n");
5951
+ }
5930
5952
  main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
5931
5953
  }
@@ -0,0 +1,2 @@
1
+ export declare const WORKFLOW_CONTRACT_VERSION = "1.0";
2
+ export declare function main(argv: string[]): Promise<number>;