@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
|
@@ -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, type ParsedArgs } from "../lib/args.js";
|
|
7
|
-
import {
|
|
7
|
+
import { atomicWriteJson, readJson, isoNow } from "../lib/fs.js";
|
|
8
8
|
|
|
9
9
|
// ─── AssignmentProvider CLI (#290) ──────────────────────────────────────────
|
|
10
10
|
// context/contracts/assignment-provider-contract.md is the governing vocabulary doc for this
|
|
@@ -60,6 +60,7 @@ export type AssignmentClaimRecord = {
|
|
|
60
60
|
subject_id: string;
|
|
61
61
|
actor: ActorStruct;
|
|
62
62
|
actor_key?: string;
|
|
63
|
+
work_item_ref?: string;
|
|
63
64
|
claimed_at: string;
|
|
64
65
|
ttl_seconds: number;
|
|
65
66
|
branch: string;
|
|
@@ -120,6 +121,8 @@ function loadActorIdentityHelper(): {
|
|
|
120
121
|
isUnresolvedActor: (actor: string) => boolean;
|
|
121
122
|
sanitizeSegment: (value: unknown) => string;
|
|
122
123
|
detectRuntime: (env: NodeJS.ProcessEnv) => string;
|
|
124
|
+
runtimeSessionId: (env: NodeJS.ProcessEnv) => string;
|
|
125
|
+
ancestorActorSeed: () => string;
|
|
123
126
|
detectCiActor: (env: NodeJS.ProcessEnv) => { runtime: string; session_id: string } | null;
|
|
124
127
|
} {
|
|
125
128
|
const _req = createRequire(import.meta.url);
|
|
@@ -130,6 +133,8 @@ function loadActorIdentityHelper(): {
|
|
|
130
133
|
isUnresolvedActor: (actor: string) => boolean;
|
|
131
134
|
sanitizeSegment: (value: unknown) => string;
|
|
132
135
|
detectRuntime: (env: NodeJS.ProcessEnv) => string;
|
|
136
|
+
runtimeSessionId: (env: NodeJS.ProcessEnv) => string;
|
|
137
|
+
ancestorActorSeed: () => string;
|
|
133
138
|
detectCiActor: (env: NodeJS.ProcessEnv) => { runtime: string; session_id: string } | null;
|
|
134
139
|
};
|
|
135
140
|
}
|
|
@@ -198,6 +203,10 @@ function loadActorStructFromFile(file: string): ActorStruct {
|
|
|
198
203
|
function loadActorStruct(args: ParsedArgs): { actor: ActorStruct; actorKey?: string } {
|
|
199
204
|
const actorJsonPath = flagString(args.flags, "actor-json");
|
|
200
205
|
if (actorJsonPath) return { actor: loadActorStructFromFile(actorJsonPath) };
|
|
206
|
+
return resolveCurrentAssignmentActor();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resolveCurrentAssignmentActor(): { actor: ActorStruct; actorKey: string } {
|
|
201
210
|
const helper = loadActorIdentityHelper();
|
|
202
211
|
const resolved = helper.resolveActor(process.env);
|
|
203
212
|
if (helper.isUnresolvedActor(resolved.actor)) throw new Error("could not resolve an actor identity (no --actor-json and no resolvable environment actor); pass --actor-json explicitly");
|
|
@@ -207,9 +216,15 @@ function loadActorStruct(args: ParsedArgs): { actor: ActorStruct; actorKey?: str
|
|
|
207
216
|
// CI session — actor_key stays correct (so no false-block), but record.actor is malformed and the
|
|
208
217
|
// audit-trail / `assignment-provider status` output for CI sessions would be corrupt.
|
|
209
218
|
const ci = resolved.source.startsWith("ci-runtime") ? helper.detectCiActor(process.env) : null;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
const runtimeSessionId = resolved.source.startsWith("runtime-session-id") ? helper.runtimeSessionId(process.env) : "";
|
|
220
|
+
const ancestrySeed = resolved.source === "process-ancestry" ? helper.ancestorActorSeed() : "";
|
|
221
|
+
const actor: ActorStruct = resolved.source === "explicit-override"
|
|
222
|
+
? { runtime: "explicit-override", session_id: resolved.actor, host: os.hostname(), human: null }
|
|
223
|
+
: ci && ci.session_id
|
|
224
|
+
? { runtime: ci.runtime, session_id: ci.session_id, host: os.hostname(), human: null }
|
|
225
|
+
: runtimeSessionId
|
|
226
|
+
? { runtime: helper.detectRuntime(process.env), session_id: runtimeSessionId, host: os.hostname(), human: null }
|
|
227
|
+
: { runtime: helper.detectRuntime(process.env), session_id: ancestrySeed ? `anc-${ancestrySeed}` : resolved.actor, host: os.hostname(), human: null };
|
|
213
228
|
return { actor, actorKey: resolved.actor };
|
|
214
229
|
}
|
|
215
230
|
|
|
@@ -218,16 +233,50 @@ export function assignmentFilePath(artifactRoot: string, subjectId: string): str
|
|
|
218
233
|
return path.join(artifactRoot, "assignment", `${sanitized}.json`);
|
|
219
234
|
}
|
|
220
235
|
|
|
236
|
+
function localAssignmentDir(artifactRoot: string, create: boolean): string | null {
|
|
237
|
+
const dir = path.join(artifactRoot, "assignment");
|
|
238
|
+
if (create) fs.mkdirSync(dir, { recursive: true });
|
|
239
|
+
let stat: fs.Stats;
|
|
240
|
+
try {
|
|
241
|
+
stat = fs.lstatSync(dir);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (!create && (error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
247
|
+
throw new Error(`assignment directory must be a real directory, not a symlink: ${dir}`);
|
|
248
|
+
}
|
|
249
|
+
const realRoot = fs.realpathSync(artifactRoot);
|
|
250
|
+
if (fs.realpathSync(dir) !== path.join(realRoot, "assignment")) {
|
|
251
|
+
throw new Error(`assignment directory escapes the artifact root: ${dir}`);
|
|
252
|
+
}
|
|
253
|
+
return dir;
|
|
254
|
+
}
|
|
255
|
+
|
|
221
256
|
export function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null {
|
|
257
|
+
if (!localAssignmentDir(artifactRoot, false)) return null;
|
|
222
258
|
const file = assignmentFilePath(artifactRoot, subjectId);
|
|
223
|
-
|
|
259
|
+
let stat: fs.Stats;
|
|
260
|
+
try {
|
|
261
|
+
stat = fs.lstatSync(file);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
267
|
+
throw new Error(`assignment record must be a regular file, not a symlink: ${file}`);
|
|
268
|
+
}
|
|
224
269
|
let data: unknown;
|
|
270
|
+
let descriptor: number | null = null;
|
|
225
271
|
try {
|
|
226
|
-
|
|
272
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
273
|
+
data = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
227
274
|
} catch (error) {
|
|
228
275
|
// Fail loud: a corrupt claim record must never be silently treated as "no claim" — that
|
|
229
276
|
// would be a fail-open path that could let a second claim silently overwrite a real one.
|
|
230
277
|
throw new Error(`assignment record is corrupt, refusing to proceed: ${file}: ${(error as Error).message}`);
|
|
278
|
+
} finally {
|
|
279
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
231
280
|
}
|
|
232
281
|
if (typeof data !== "object" || data === null) throw new Error(`assignment record is not an object: ${file}`);
|
|
233
282
|
const record = data as AssignmentClaimRecord;
|
|
@@ -239,7 +288,7 @@ export function writeLocalRecord(artifactRoot: string, subjectId: string, record
|
|
|
239
288
|
// writeJson throws on any mkdir/writeFileSync failure; that error is intentionally allowed to
|
|
240
289
|
// propagate to main()'s top-level try/catch and exit non-zero. Durable writes must fail loud,
|
|
241
290
|
// never fail open (artifact-contract.md).
|
|
242
|
-
|
|
291
|
+
atomicWriteJson(artifactRoot, assignmentFilePath(artifactRoot, subjectId), record);
|
|
243
292
|
}
|
|
244
293
|
|
|
245
294
|
/**
|
|
@@ -255,8 +304,7 @@ function sleepSync(ms: number): void {
|
|
|
255
304
|
}
|
|
256
305
|
|
|
257
306
|
function subjectLockDir(artifactRoot: string, subjectId: string): string {
|
|
258
|
-
const assignmentDir =
|
|
259
|
-
fs.mkdirSync(assignmentDir, { recursive: true });
|
|
307
|
+
const assignmentDir = localAssignmentDir(artifactRoot, true)!;
|
|
260
308
|
const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
|
|
261
309
|
return path.join(assignmentDir, `.${sanitized}.lockdir`);
|
|
262
310
|
}
|
|
@@ -310,11 +358,19 @@ export function withSubjectLock<T>(artifactRoot: string, subjectId: string, body
|
|
|
310
358
|
sleepSync(20);
|
|
311
359
|
}
|
|
312
360
|
}
|
|
361
|
+
const release = (): void => fs.rmSync(lockDir, { recursive: true, force: true });
|
|
362
|
+
let result: T;
|
|
313
363
|
try {
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
364
|
+
result = body();
|
|
365
|
+
} catch (error) {
|
|
366
|
+
release();
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
if (result && typeof (result as { then?: unknown }).then === "function") {
|
|
370
|
+
return Promise.resolve(result).finally(release) as T;
|
|
317
371
|
}
|
|
372
|
+
release();
|
|
373
|
+
return result;
|
|
318
374
|
}
|
|
319
375
|
|
|
320
376
|
/**
|
|
@@ -512,7 +568,7 @@ export function performLocalClaim(
|
|
|
512
568
|
artifactRoot: string,
|
|
513
569
|
subjectId: string,
|
|
514
570
|
actor: ActorStruct,
|
|
515
|
-
opts: { ttlSeconds: number; branch: string; artifactDir: string; reason?: string; actorKey?: string },
|
|
571
|
+
opts: { ttlSeconds: number; branch: string; artifactDir: string; reason?: string; actorKey?: string; workItemRef?: string },
|
|
516
572
|
): AssignmentClaimRecord {
|
|
517
573
|
const helper = loadActorIdentityHelper();
|
|
518
574
|
const reason = opts.reason ?? "claim";
|
|
@@ -538,6 +594,7 @@ export function performLocalClaim(
|
|
|
538
594
|
subject_id: subjectId,
|
|
539
595
|
actor,
|
|
540
596
|
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
597
|
+
...((opts.workItemRef ?? existing?.work_item_ref) ? { work_item_ref: opts.workItemRef ?? existing?.work_item_ref } : {}),
|
|
541
598
|
claimed_at: isoNow(),
|
|
542
599
|
ttl_seconds: opts.ttlSeconds,
|
|
543
600
|
branch: opts.branch,
|
|
@@ -615,15 +672,22 @@ export function performLocalRelease(
|
|
|
615
672
|
subjectId: string,
|
|
616
673
|
releasedBy: ActorStruct | null,
|
|
617
674
|
opts: { reason?: string; actorKey?: string; tolerateNoActiveClaim?: boolean } = {},
|
|
675
|
+
): AssignmentClaimRecord | null {
|
|
676
|
+
return withSubjectLock(artifactRoot, subjectId, () => performLocalReleaseUnderLock(artifactRoot, subjectId, releasedBy, opts));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Caller must already hold this subject's assignment lock through withSubjectLock(). */
|
|
680
|
+
export function performLocalReleaseUnderLock(
|
|
681
|
+
artifactRoot: string,
|
|
682
|
+
subjectId: string,
|
|
683
|
+
releasedBy: ActorStruct | null,
|
|
684
|
+
opts: { reason?: string; actorKey?: string; tolerateNoActiveClaim?: boolean } = {},
|
|
618
685
|
): AssignmentClaimRecord | null {
|
|
619
686
|
const helper = loadActorIdentityHelper();
|
|
620
687
|
const reason = opts.reason ?? "released";
|
|
621
688
|
const tolerateNoActiveClaim = opts.tolerateNoActiveClaim ?? false;
|
|
622
689
|
|
|
623
|
-
|
|
624
|
-
// do, under the same per-subject lock (see withSubjectLock()'s doc comment).
|
|
625
|
-
return withSubjectLock(artifactRoot, subjectId, (): AssignmentClaimRecord | null => {
|
|
626
|
-
const existing = readLocalRecord(artifactRoot, subjectId);
|
|
690
|
+
const existing = readLocalRecord(artifactRoot, subjectId);
|
|
627
691
|
if (!existing || existing.status !== "claimed") {
|
|
628
692
|
if (tolerateNoActiveClaim) return null;
|
|
629
693
|
throw new Error(`no active claim to release for subject: ${subjectId}`);
|
|
@@ -675,8 +739,7 @@ export function performLocalRelease(
|
|
|
675
739
|
audit_trail: [...(existing.audit_trail ?? []), { at: isoNow(), transition: "release", from_actor: existing.actor, to_actor: releasedBy, reason }],
|
|
676
740
|
};
|
|
677
741
|
writeLocalRecord(artifactRoot, subjectId, record);
|
|
678
|
-
|
|
679
|
-
});
|
|
742
|
+
return record;
|
|
680
743
|
}
|
|
681
744
|
|
|
682
745
|
function releaseLocalFile(argv: string[]): number {
|
|
@@ -707,7 +770,7 @@ export function performLocalSupersede(
|
|
|
707
770
|
subjectId: string,
|
|
708
771
|
fromActor: ActorStruct,
|
|
709
772
|
toActor: ActorStruct,
|
|
710
|
-
opts: { ttlSeconds?: number; branch?: string; artifactDir?: string; reason?: string; actorKey?: string } = {},
|
|
773
|
+
opts: { ttlSeconds?: number; branch?: string; artifactDir?: string; reason?: string; actorKey?: string; workItemRef?: string } = {},
|
|
711
774
|
): AssignmentClaimRecord {
|
|
712
775
|
const helper = loadActorIdentityHelper();
|
|
713
776
|
const reason = opts.reason ?? "supersede";
|
|
@@ -732,6 +795,7 @@ export function performLocalSupersede(
|
|
|
732
795
|
subject_id: subjectId,
|
|
733
796
|
actor: toActor,
|
|
734
797
|
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
798
|
+
...((opts.workItemRef ?? existing.work_item_ref) ? { work_item_ref: opts.workItemRef ?? existing.work_item_ref } : {}),
|
|
735
799
|
claimed_at: isoNow(),
|
|
736
800
|
ttl_seconds: ttlSeconds,
|
|
737
801
|
branch: opts.branch ?? existing.branch,
|