@kontourai/flow-agents 3.4.3 → 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 +15 -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 +13 -0
- package/build/src/cli/assignment-provider.js +120 -62
- 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 +140 -30
- 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/flow-resolver.js +7 -2
- 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/contracts/assignment-provider-contract.md +5 -2
- 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 +49 -5
- package/docs/workflow-usage-guide.md +5 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_assignment_provider_local_file.sh +54 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -24
- package/evals/integration/test_bundle_install.sh +97 -0
- package/evals/integration/test_current_json_per_actor.sh +1 -0
- package/evals/integration/test_dual_emit_flow_step.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
- package/evals/integration/test_phase_map_and_gate_claim.sh +4 -0
- package/evals/integration/test_public_workflow_cli.sh +259 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1 -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 +84 -20
- 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 +138 -31
- package/src/cli/workflow.ts +471 -0
- package/src/cli.ts +2 -0
- package/src/index.ts +14 -0
- package/src/lib/flow-resolver.ts +6 -2
- package/src/lib/package-version.ts +15 -0
- package/src/lib/pinned-cli-command.ts +23 -0
|
@@ -31,6 +31,7 @@ export type AssignmentClaimRecord = {
|
|
|
31
31
|
subject_id: string;
|
|
32
32
|
actor: ActorStruct;
|
|
33
33
|
actor_key?: string;
|
|
34
|
+
work_item_ref?: string;
|
|
34
35
|
claimed_at: string;
|
|
35
36
|
ttl_seconds: number;
|
|
36
37
|
branch: string;
|
|
@@ -53,6 +54,10 @@ export type AssignmentStatus = {
|
|
|
53
54
|
record: AssignmentClaimRecord | null;
|
|
54
55
|
has_claim_label?: boolean;
|
|
55
56
|
};
|
|
57
|
+
export declare function resolveCurrentAssignmentActor(): {
|
|
58
|
+
actor: ActorStruct;
|
|
59
|
+
actorKey: string;
|
|
60
|
+
};
|
|
56
61
|
export declare function assignmentFilePath(artifactRoot: string, subjectId: string): string;
|
|
57
62
|
export declare function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null;
|
|
58
63
|
export declare function writeLocalRecord(artifactRoot: string, subjectId: string, record: AssignmentClaimRecord): void;
|
|
@@ -116,6 +121,7 @@ export declare function performLocalClaim(artifactRoot: string, subjectId: strin
|
|
|
116
121
|
artifactDir: string;
|
|
117
122
|
reason?: string;
|
|
118
123
|
actorKey?: string;
|
|
124
|
+
workItemRef?: string;
|
|
119
125
|
}): AssignmentClaimRecord;
|
|
120
126
|
/**
|
|
121
127
|
* Wave 1 (#292) extraction: the durable-write body previously inlined inside releaseLocalFile's
|
|
@@ -162,6 +168,12 @@ export declare function performLocalRelease(artifactRoot: string, subjectId: str
|
|
|
162
168
|
actorKey?: string;
|
|
163
169
|
tolerateNoActiveClaim?: boolean;
|
|
164
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;
|
|
165
177
|
/**
|
|
166
178
|
* Wave 1 (#291) extraction: the durable-write body previously inlined inside supersedeLocalFile's
|
|
167
179
|
* withSubjectLock() closure, now a parameter-driven pure function so ensure-session's
|
|
@@ -177,6 +189,7 @@ export declare function performLocalSupersede(artifactRoot: string, subjectId: s
|
|
|
177
189
|
artifactDir?: string;
|
|
178
190
|
reason?: string;
|
|
179
191
|
actorKey?: string;
|
|
192
|
+
workItemRef?: string;
|
|
180
193
|
}): AssignmentClaimRecord;
|
|
181
194
|
/**
|
|
182
195
|
* Wave 1 (#291) extraction: the local-file branch of statusCommand's assignment-layer read,
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { parseArgs, flagString } from "../lib/args.js";
|
|
7
|
-
import {
|
|
7
|
+
import { atomicWriteJson, readJson, isoNow } from "../lib/fs.js";
|
|
8
8
|
const DEFAULT_LABEL_NAME = "agent:claimed";
|
|
9
9
|
const CLAIM_COMMENT_MARKER_DEFAULT = "<!-- flow-agents:assignment-claim -->";
|
|
10
10
|
/**
|
|
@@ -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,28 +89,74 @@ 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) {
|
|
95
104
|
const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
|
|
96
105
|
return path.join(artifactRoot, "assignment", `${sanitized}.json`);
|
|
97
106
|
}
|
|
107
|
+
function localAssignmentDir(artifactRoot, create) {
|
|
108
|
+
const dir = path.join(artifactRoot, "assignment");
|
|
109
|
+
if (create)
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
let stat;
|
|
112
|
+
try {
|
|
113
|
+
stat = fs.lstatSync(dir);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!create && error.code === "ENOENT")
|
|
117
|
+
return null;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
121
|
+
throw new Error(`assignment directory must be a real directory, not a symlink: ${dir}`);
|
|
122
|
+
}
|
|
123
|
+
const realRoot = fs.realpathSync(artifactRoot);
|
|
124
|
+
if (fs.realpathSync(dir) !== path.join(realRoot, "assignment")) {
|
|
125
|
+
throw new Error(`assignment directory escapes the artifact root: ${dir}`);
|
|
126
|
+
}
|
|
127
|
+
return dir;
|
|
128
|
+
}
|
|
98
129
|
export function readLocalRecord(artifactRoot, subjectId) {
|
|
99
|
-
|
|
100
|
-
if (!fs.existsSync(file))
|
|
130
|
+
if (!localAssignmentDir(artifactRoot, false))
|
|
101
131
|
return null;
|
|
132
|
+
const file = assignmentFilePath(artifactRoot, subjectId);
|
|
133
|
+
let stat;
|
|
134
|
+
try {
|
|
135
|
+
stat = fs.lstatSync(file);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code === "ENOENT")
|
|
139
|
+
return null;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
143
|
+
throw new Error(`assignment record must be a regular file, not a symlink: ${file}`);
|
|
144
|
+
}
|
|
102
145
|
let data;
|
|
146
|
+
let descriptor = null;
|
|
103
147
|
try {
|
|
104
|
-
|
|
148
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
149
|
+
data = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
105
150
|
}
|
|
106
151
|
catch (error) {
|
|
107
152
|
// Fail loud: a corrupt claim record must never be silently treated as "no claim" — that
|
|
108
153
|
// would be a fail-open path that could let a second claim silently overwrite a real one.
|
|
109
154
|
throw new Error(`assignment record is corrupt, refusing to proceed: ${file}: ${error.message}`);
|
|
110
155
|
}
|
|
156
|
+
finally {
|
|
157
|
+
if (descriptor !== null)
|
|
158
|
+
fs.closeSync(descriptor);
|
|
159
|
+
}
|
|
111
160
|
if (typeof data !== "object" || data === null)
|
|
112
161
|
throw new Error(`assignment record is not an object: ${file}`);
|
|
113
162
|
const record = data;
|
|
@@ -119,7 +168,7 @@ export function writeLocalRecord(artifactRoot, subjectId, record) {
|
|
|
119
168
|
// writeJson throws on any mkdir/writeFileSync failure; that error is intentionally allowed to
|
|
120
169
|
// propagate to main()'s top-level try/catch and exit non-zero. Durable writes must fail loud,
|
|
121
170
|
// never fail open (artifact-contract.md).
|
|
122
|
-
|
|
171
|
+
atomicWriteJson(artifactRoot, assignmentFilePath(artifactRoot, subjectId), record);
|
|
123
172
|
}
|
|
124
173
|
/**
|
|
125
174
|
* Synchronous busy-sleep via Atomics.wait on a throwaway SharedArrayBuffer — Node.js (unlike
|
|
@@ -133,8 +182,7 @@ function sleepSync(ms) {
|
|
|
133
182
|
Atomics.wait(ia, 0, 0, ms);
|
|
134
183
|
}
|
|
135
184
|
function subjectLockDir(artifactRoot, subjectId) {
|
|
136
|
-
const assignmentDir =
|
|
137
|
-
fs.mkdirSync(assignmentDir, { recursive: true });
|
|
185
|
+
const assignmentDir = localAssignmentDir(artifactRoot, true);
|
|
138
186
|
const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
|
|
139
187
|
return path.join(assignmentDir, `.${sanitized}.lockdir`);
|
|
140
188
|
}
|
|
@@ -190,12 +238,20 @@ export function withSubjectLock(artifactRoot, subjectId, body) {
|
|
|
190
238
|
sleepSync(20);
|
|
191
239
|
}
|
|
192
240
|
}
|
|
241
|
+
const release = () => fs.rmSync(lockDir, { recursive: true, force: true });
|
|
242
|
+
let result;
|
|
193
243
|
try {
|
|
194
|
-
|
|
244
|
+
result = body();
|
|
195
245
|
}
|
|
196
|
-
|
|
197
|
-
|
|
246
|
+
catch (error) {
|
|
247
|
+
release();
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
if (result && typeof result.then === "function") {
|
|
251
|
+
return Promise.resolve(result).finally(release);
|
|
198
252
|
}
|
|
253
|
+
release();
|
|
254
|
+
return result;
|
|
199
255
|
}
|
|
200
256
|
/**
|
|
201
257
|
* The assignment ⋈ liveness join (contract doc's "assignment ⋈ liveness join" section, ADR 0021
|
|
@@ -403,6 +459,7 @@ export function performLocalClaim(artifactRoot, subjectId, actor, opts) {
|
|
|
403
459
|
subject_id: subjectId,
|
|
404
460
|
actor,
|
|
405
461
|
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
462
|
+
...((opts.workItemRef ?? existing?.work_item_ref) ? { work_item_ref: opts.workItemRef ?? existing?.work_item_ref } : {}),
|
|
406
463
|
claimed_at: isoNow(),
|
|
407
464
|
ttl_seconds: opts.ttlSeconds,
|
|
408
465
|
branch: opts.branch,
|
|
@@ -474,61 +531,61 @@ function claimLocalFile(argv) {
|
|
|
474
531
|
* than allowed to silently no-op or wrongly refuse.
|
|
475
532
|
*/
|
|
476
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 = {}) {
|
|
477
538
|
const helper = loadActorIdentityHelper();
|
|
478
539
|
const reason = opts.reason ?? "released";
|
|
479
540
|
const tolerateNoActiveClaim = opts.tolerateNoActiveClaim ?? false;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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}`);
|
|
486
557
|
return null;
|
|
487
|
-
throw new Error(`no active claim to release for subject: ${subjectId}`);
|
|
488
|
-
}
|
|
489
|
-
if (releasedBy) {
|
|
490
|
-
// Contract guard (hardening fix, #292 review): a caller that supplies `releasedBy` but NOT
|
|
491
|
-
// `opts.actorKey` against a record that already carries `actor_key` cannot reliably prove
|
|
492
|
-
// ownership — see this function's doc comment. This is the ONLY combination that fires: it
|
|
493
|
-
// does NOT fire when `existing.actor_key` is absent (the CLI/fixture path, where both sides
|
|
494
|
-
// fall back to serializeActor() and legitimately compare equal). Fail loudly rather than
|
|
495
|
-
// silently no-op (tolerant callers) or wrongly refuse (throwing callers) — never silent.
|
|
496
|
-
if (!opts.actorKey && existing.actor_key) {
|
|
497
|
-
if (tolerateNoActiveClaim) {
|
|
498
|
-
console.error(`[performLocalRelease] cannot verify ownership of an actor_key-stamped record without opts.actorKey; skipping release for ${subjectId}`);
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
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");
|
|
502
|
-
}
|
|
503
|
-
// AC6: never force-release a claim held by a different actor. Mirrors
|
|
504
|
-
// computeEffectiveState()'s canonical self-recognition comparison EXACTLY —
|
|
505
|
-
// `holderActorKey` prefers the stored `actor_key` (the canonical resolveActor(env).actor
|
|
506
|
-
// string, present on records written by the fixed performLocalClaim/performLocalSupersede
|
|
507
|
-
// paths) and only falls back to `serializeActor(existing.actor)` when `actor_key` is
|
|
508
|
-
// absent (every pre-fix record, every #290 eval fixture). The releaser's side must use the
|
|
509
|
-
// SAME canonical form: `opts.actorKey` (the caller's resolveActor(env).actor string, e.g.
|
|
510
|
-
// scripts/hooks/stop-goal-fit.js's Stop hook) when provided, else re-derived via
|
|
511
|
-
// serializeActor(releasedBy) — never serializeActor() unconditionally on both sides, which
|
|
512
|
-
// would compare the bare actor_key form against a re-derived triple form for an
|
|
513
|
-
// explicit-override actor and spuriously reject a legitimate same-actor release (the #291
|
|
514
|
-
// seam, relocated to this write path).
|
|
515
|
-
const holderActorKey = existing.actor_key || helper.serializeActor(existing.actor);
|
|
516
|
-
const releasedByActorKey = opts.actorKey || helper.serializeActor(releasedBy);
|
|
517
|
-
if (holderActorKey !== releasedByActorKey) {
|
|
518
|
-
if (tolerateNoActiveClaim)
|
|
519
|
-
return null;
|
|
520
|
-
throw new Error(`--actor-json does not match the current holder (${holderActorKey}); refusing to release a claim held by someone else`);
|
|
521
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");
|
|
522
560
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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;
|
|
532
589
|
}
|
|
533
590
|
function releaseLocalFile(argv) {
|
|
534
591
|
const args = parseArgs(argv);
|
|
@@ -574,6 +631,7 @@ export function performLocalSupersede(artifactRoot, subjectId, fromActor, toActo
|
|
|
574
631
|
subject_id: subjectId,
|
|
575
632
|
actor: toActor,
|
|
576
633
|
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
634
|
+
...((opts.workItemRef ?? existing.work_item_ref) ? { work_item_ref: opts.workItemRef ?? existing.work_item_ref } : {}),
|
|
577
635
|
claimed_at: isoNow(),
|
|
578
636
|
ttl_seconds: ttlSeconds,
|
|
579
637
|
branch: opts.branch ?? existing.branch,
|
|
@@ -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>;
|