@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.
- package/.github/workflows/ci.yml +4 -0
- package/CHANGELOG.md +8 -0
- package/build/src/builder-flow-run-adapter.d.ts +10 -1
- package/build/src/builder-flow-run-adapter.js +29 -1
- package/build/src/builder-flow-runtime.d.ts +18 -0
- package/build/src/builder-flow-runtime.js +205 -13
- package/build/src/builder-lifecycle-authority.d.ts +35 -0
- package/build/src/builder-lifecycle-authority.js +219 -0
- package/build/src/cli/assignment-provider.d.ts +10 -0
- package/build/src/cli/assignment-provider.js +61 -52
- package/build/src/cli/builder-run.js +46 -5
- package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
- package/build/src/cli/workflow-sidecar.d.ts +3 -0
- package/build/src/cli/workflow-sidecar.js +28 -6
- package/build/src/cli/workflow.d.ts +2 -0
- package/build/src/cli/workflow.js +521 -0
- package/build/src/cli.js +2 -0
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +2 -0
- package/build/src/lib/package-version.d.ts +2 -0
- package/build/src/lib/package-version.js +13 -0
- package/build/src/lib/pinned-cli-command.d.ts +6 -0
- package/build/src/lib/pinned-cli-command.js +21 -0
- package/context/contracts/artifact-contract.md +1 -1
- package/context/scripts/hooks/config-protection.js +8 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/context/scripts/hooks/stop-goal-fit.js +1 -1
- package/docs/context-map.md +2 -0
- package/docs/public-workflow-cli.md +63 -0
- package/docs/spec/builder-flow-runtime.md +37 -0
- package/docs/workflow-usage-guide.md +5 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -4
- package/evals/integration/test_bundle_install.sh +59 -5
- package/evals/integration/test_public_workflow_cli.sh +259 -0
- package/package.json +2 -2
- package/schemas/builder-lifecycle-authorization.schema.json +57 -0
- package/schemas/lifecycle-authority-keys.schema.json +25 -0
- package/schemas/workflow-state.schema.json +1 -1
- package/scripts/hooks/config-protection.js +8 -1
- package/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/scripts/hooks/stop-goal-fit.js +1 -1
- package/src/builder-flow-run-adapter.ts +47 -0
- package/src/builder-flow-runtime.ts +216 -4
- package/src/builder-lifecycle-authority.ts +218 -0
- package/src/cli/assignment-provider.ts +29 -9
- package/src/cli/builder-flow-runtime.test.mjs +404 -1
- package/src/cli/builder-run.ts +56 -5
- package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
- package/src/cli/workflow-sidecar.ts +28 -6
- package/src/cli/workflow.ts +471 -0
- package/src/cli.ts +2 -0
- package/src/index.ts +14 -0
- package/src/lib/package-version.ts +15 -0
- 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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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(
|
|
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
|
|
19
|
-
console.error(
|
|
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
|
-
:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
}
|