@kontourai/flow-agents 3.4.3 → 3.5.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/CHANGELOG.md +7 -0
- package/build/src/cli/assignment-provider.d.ts +3 -0
- package/build/src/cli/assignment-provider.js +59 -10
- package/build/src/cli/workflow-sidecar.js +112 -24
- package/build/src/lib/flow-resolver.js +7 -2
- package/context/contracts/assignment-provider-contract.md +5 -2
- package/docs/spec/builder-flow-runtime.md +12 -5
- package/evals/integration/test_assignment_provider_local_file.sh +54 -0
- package/evals/integration/test_builder_entry_enforcement.sh +236 -20
- package/evals/integration/test_bundle_install.sh +43 -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_workflow_sidecar_writer.sh +1 -0
- package/package.json +1 -1
- package/src/cli/assignment-provider.ts +55 -11
- package/src/cli/workflow-sidecar.ts +110 -25
- package/src/lib/flow-resolver.ts +6 -2
|
@@ -50,6 +50,48 @@ else
|
|
|
50
50
|
fail "symlinked Builder root wrote externally or returned the wrong diagnostic: $(cat "$TMP/symlink-root.out")"
|
|
51
51
|
fi
|
|
52
52
|
|
|
53
|
+
SESSION_SYMLINK_PROJECT="$TMP/session-symlink-project"
|
|
54
|
+
SESSION_SYMLINK_ROOT="$SESSION_SYMLINK_PROJECT/.kontourai/flow-agents"
|
|
55
|
+
SESSION_SYMLINK_EXTERNAL="$TMP/session-symlink-external"
|
|
56
|
+
mkdir -p "$SESSION_SYMLINK_ROOT" "$SESSION_SYMLINK_EXTERNAL"
|
|
57
|
+
ln -s "$SESSION_SYMLINK_EXTERNAL" "$SESSION_SYMLINK_ROOT/session-symlink"
|
|
58
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
59
|
+
--artifact-root "$SESSION_SYMLINK_ROOT" \
|
|
60
|
+
--task-slug session-symlink \
|
|
61
|
+
--actor builder-entry-session-symlink \
|
|
62
|
+
--title "Session symlink" \
|
|
63
|
+
--summary "Session state must not escape the artifact root." \
|
|
64
|
+
--flow-id builder.build >"$TMP/session-symlink.out" 2>&1; then
|
|
65
|
+
fail "symlinked session directory should be rejected"
|
|
66
|
+
elif [[ -z "$(find "$SESSION_SYMLINK_EXTERNAL" -mindepth 1 -print -quit)" ]] \
|
|
67
|
+
&& grep -q 'session directory must be a real directory under the artifact root' "$TMP/session-symlink.out"; then
|
|
68
|
+
pass "symlinked session directory is rejected before external reads or writes"
|
|
69
|
+
else
|
|
70
|
+
fail "symlinked session directory escaped or returned the wrong diagnostic: $(cat "$TMP/session-symlink.out")"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
NESTED_PROJECT="$TMP/nested-symlink-project"
|
|
74
|
+
NESTED_ROOT="$NESTED_PROJECT/.kontourai/flow-agents"
|
|
75
|
+
NESTED_EXTERNAL="$TMP/nested-symlink-external.json"
|
|
76
|
+
mkdir -p "$NESTED_ROOT/assignment"
|
|
77
|
+
printf 'external must survive\n' >"$NESTED_EXTERNAL"
|
|
78
|
+
ln -s "$NESTED_EXTERNAL" "$NESTED_ROOT/assignment/nested-symlink.json"
|
|
79
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
80
|
+
--artifact-root "$NESTED_ROOT" \
|
|
81
|
+
--task-slug nested-symlink \
|
|
82
|
+
--actor builder-entry-nested-symlink \
|
|
83
|
+
--title "Nested assignment symlink" \
|
|
84
|
+
--summary "Assignment evidence must stay inside the product root." \
|
|
85
|
+
--flow-id builder.build >"$TMP/nested-symlink.out" 2>&1; then
|
|
86
|
+
fail "symlinked assignment record should be rejected"
|
|
87
|
+
elif [[ "$(cat "$NESTED_EXTERNAL")" == "external must survive" ]] \
|
|
88
|
+
&& [[ ! -e "$NESTED_ROOT/nested-symlink" ]] \
|
|
89
|
+
&& grep -q 'assignment record must be a regular file, not a symlink' "$TMP/nested-symlink.out"; then
|
|
90
|
+
pass "nested assignment symlink is rejected before external write or trust evidence"
|
|
91
|
+
else
|
|
92
|
+
fail "nested assignment symlink escaped or returned the wrong diagnostic: $(cat "$TMP/nested-symlink.out")"
|
|
93
|
+
fi
|
|
94
|
+
|
|
53
95
|
RACE_PROJECT="$TMP/race-project"
|
|
54
96
|
RACE_ROOT="$RACE_PROJECT/.kontourai/flow-agents"
|
|
55
97
|
RACE_MOVED="$RACE_PROJECT/.kontourai/flow-agents-acquired"
|
|
@@ -146,6 +188,7 @@ LOCAL_ROOT="$TMP/local/.kontourai/flow-agents"
|
|
|
146
188
|
if flow_agents_node "$WRITER" ensure-session \
|
|
147
189
|
--artifact-root "$LOCAL_ROOT" \
|
|
148
190
|
--task-slug local-request \
|
|
191
|
+
--actor builder-entry-local \
|
|
149
192
|
--title "Local request" \
|
|
150
193
|
--summary "Providerless work still needs an anchor." \
|
|
151
194
|
--flow-id builder.build \
|
|
@@ -158,19 +201,24 @@ const current = JSON.parse(fs.readFileSync(path.join(root, 'current.json'), 'utf
|
|
|
158
201
|
const state = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'state.json'), 'utf8'));
|
|
159
202
|
const workItem = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'work-item.json'), 'utf8'));
|
|
160
203
|
const flowState = JSON.parse(fs.readFileSync(path.join(path.dirname(root), 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
|
|
161
|
-
if (current.active_flow_id !== 'builder.build' || current.active_step_id !== '
|
|
162
|
-
if (state.status !== '
|
|
204
|
+
if (current.active_flow_id !== 'builder.build' || current.active_step_id !== 'design-probe') process.exit(1);
|
|
205
|
+
if (state.status !== 'in_progress' || state.phase !== 'pickup') process.exit(1);
|
|
163
206
|
if (JSON.stringify(state.work_item_refs) !== JSON.stringify(['local:local-request'])) process.exit(1);
|
|
164
207
|
if (workItem.id !== 'local-request' || workItem.title !== 'Local request') process.exit(1);
|
|
165
208
|
if (workItem.source_provider?.kind !== 'local' || workItem.source_provider?.path !== 'work-item.json') process.exit(1);
|
|
166
|
-
if (flowState.current_step !== '
|
|
167
|
-
if (state.flow_run?.current_step !== '
|
|
168
|
-
if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['
|
|
209
|
+
if (flowState.current_step !== 'design-probe' || flowState.subject !== 'local:local-request') process.exit(1);
|
|
210
|
+
if (state.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
211
|
+
if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
169
212
|
if (!state.next_action?.command?.includes('builder-run sync')) process.exit(1);
|
|
170
213
|
if ('enforcement' in state.next_action) process.exit(1);
|
|
214
|
+
const bundle = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'trust.bundle'), 'utf8'));
|
|
215
|
+
const selected = (bundle.claims || []).find((claim) => claim.claimType === 'builder.pull-work.selected');
|
|
216
|
+
if (selected?.status !== 'verified') process.exit(1);
|
|
217
|
+
if (selected?.metadata?.workflow_subject_ref !== 'local:local-request') process.exit(1);
|
|
218
|
+
if (!(selected?.metadata?.artifact_refs || []).some((ref) => ref.file === '.kontourai/flow-agents/assignment/local-request.json')) process.exit(1);
|
|
171
219
|
NODE
|
|
172
220
|
then
|
|
173
|
-
pass "
|
|
221
|
+
pass "durably acquired local Work Item satisfies pull-work through the trust bundle and Flow advances"
|
|
174
222
|
else
|
|
175
223
|
fail "local Work Item or first-step state is invalid"
|
|
176
224
|
fi
|
|
@@ -183,6 +231,7 @@ FLOW_DIGEST_BEFORE="$(find "$TMP/local/.kontourai/flow/runs/local-request" -type
|
|
|
183
231
|
if flow_agents_node "$WRITER" ensure-session \
|
|
184
232
|
--artifact-root "$LOCAL_ROOT" \
|
|
185
233
|
--task-slug local-request \
|
|
234
|
+
--actor builder-entry-local \
|
|
186
235
|
--title "Local request" \
|
|
187
236
|
--summary "Providerless work still needs an anchor." \
|
|
188
237
|
--flow-id builder.build \
|
|
@@ -195,9 +244,9 @@ const project = process.argv[2];
|
|
|
195
244
|
const session = process.argv[3];
|
|
196
245
|
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
|
|
197
246
|
const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
|
|
198
|
-
if (flowState.current_step !== '
|
|
199
|
-
if (sidecar.flow_run?.current_step !== '
|
|
200
|
-
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['
|
|
247
|
+
if (flowState.current_step !== 'design-probe' || flowState.subject !== 'local:local-request') process.exit(1);
|
|
248
|
+
if (sidecar.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
249
|
+
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
201
250
|
if (!sidecar.next_action?.command?.includes('builder-run sync')) process.exit(1);
|
|
202
251
|
NODE
|
|
203
252
|
then
|
|
@@ -213,14 +262,15 @@ printf 'not a run-store directory\n' > "$BROKEN_PROJECT/.kontourai/flow"
|
|
|
213
262
|
if flow_agents_node "$WRITER" ensure-session \
|
|
214
263
|
--artifact-root "$BROKEN_ROOT" \
|
|
215
264
|
--task-slug broken-start \
|
|
265
|
+
--actor builder-entry-broken \
|
|
216
266
|
--title "Broken start" \
|
|
217
267
|
--summary "Flow startup must fail visibly." \
|
|
218
268
|
--flow-id builder.build >"$TMP/broken-start.out" 2>&1; then
|
|
219
269
|
fail "invalid canonical Flow startup should fail ensure-session"
|
|
220
270
|
elif [[ -f "$BROKEN_ROOT/broken-start/state.json" ]] \
|
|
221
271
|
&& [[ ! -e "$BROKEN_PROJECT/.kontourai/flow/runs/broken-start" ]] \
|
|
222
|
-
&& grep -q 'canonical Builder Flow
|
|
223
|
-
&& grep -q '
|
|
272
|
+
&& grep -q 'canonical Builder Flow entry failed' "$TMP/broken-start.out" \
|
|
273
|
+
&& grep -q 'Re-run the same ensure-session command' "$TMP/broken-start.out" \
|
|
224
274
|
&& node - "$BROKEN_ROOT/broken-start/state.json" <<'NODE'
|
|
225
275
|
const fs = require('node:fs');
|
|
226
276
|
const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
@@ -233,13 +283,30 @@ else
|
|
|
233
283
|
fail "failed canonical startup forged state or lost recovery guidance: $(cat "$TMP/broken-start.out")"
|
|
234
284
|
fi
|
|
235
285
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
--
|
|
239
|
-
--
|
|
240
|
-
--
|
|
241
|
-
|
|
242
|
-
|
|
286
|
+
rm -f "$BROKEN_PROJECT/.kontourai/flow"
|
|
287
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
288
|
+
--artifact-root "$BROKEN_ROOT" \
|
|
289
|
+
--task-slug broken-start \
|
|
290
|
+
--actor builder-entry-broken \
|
|
291
|
+
--title "Broken start" \
|
|
292
|
+
--summary "Flow startup must recover from persisted acquisition provenance." \
|
|
293
|
+
--flow-id builder.build >"$TMP/broken-start-retry.out" 2>&1 \
|
|
294
|
+
&& node - "$BROKEN_PROJECT" <<'NODE'
|
|
295
|
+
const fs = require('node:fs');
|
|
296
|
+
const path = require('node:path');
|
|
297
|
+
const project = process.argv[2];
|
|
298
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'broken-start', 'state.json'), 'utf8'));
|
|
299
|
+
const bundle = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'broken-start', 'trust.bundle'), 'utf8'));
|
|
300
|
+
if (flowState.current_step !== 'design-probe') process.exit(1);
|
|
301
|
+
if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) process.exit(1);
|
|
302
|
+
NODE
|
|
303
|
+
then
|
|
304
|
+
pass "interrupted Flow startup retries from exact persisted acquisition provenance"
|
|
305
|
+
else
|
|
306
|
+
fail "interrupted acquisition could not recover: $(cat "$TMP/broken-start-retry.out")"
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
|
|
243
310
|
const fs = require('node:fs');
|
|
244
311
|
const path = require('node:path');
|
|
245
312
|
const project = process.argv[2];
|
|
@@ -251,9 +318,158 @@ if (sidecar.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
|
251
318
|
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
252
319
|
NODE
|
|
253
320
|
then
|
|
254
|
-
pass "
|
|
321
|
+
pass "automatic selected-work claim synchronizes Flow and projects the next skill"
|
|
322
|
+
else
|
|
323
|
+
fail "automatic selected-work claim did not advance the canonical run"
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
SKIPPED_ROOT="$TMP/skipped/.kontourai/flow-agents"
|
|
327
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
328
|
+
--artifact-root "$SKIPPED_ROOT" \
|
|
329
|
+
--task-slug skipped-ownership \
|
|
330
|
+
--actor builder-entry-skipped \
|
|
331
|
+
--skip-ownership-guard \
|
|
332
|
+
--title "Skipped ownership" \
|
|
333
|
+
--summary "Unproven ownership must remain at pull-work." \
|
|
334
|
+
--flow-id builder.build >"$TMP/skipped.out" 2>&1 \
|
|
335
|
+
&& node - "$TMP/skipped" <<'NODE'
|
|
336
|
+
const fs = require('node:fs');
|
|
337
|
+
const path = require('node:path');
|
|
338
|
+
const project = process.argv[2];
|
|
339
|
+
const session = path.join(project, '.kontourai', 'flow-agents', 'skipped-ownership');
|
|
340
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'skipped-ownership', 'state.json'), 'utf8'));
|
|
341
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
|
|
342
|
+
if (flowState.current_step !== 'pull-work' || sidecar.flow_run?.current_step !== 'pull-work') process.exit(1);
|
|
343
|
+
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
|
|
344
|
+
if (fs.existsSync(path.join(session, 'trust.bundle'))) process.exit(1);
|
|
345
|
+
if (fs.existsSync(path.join(project, '.kontourai', 'flow-agents', 'assignment', 'skipped-ownership.json'))) process.exit(1);
|
|
346
|
+
NODE
|
|
347
|
+
then
|
|
348
|
+
pass "skipped ownership remains at pull-work without selected-work evidence"
|
|
349
|
+
else
|
|
350
|
+
fail "unproven ownership advanced the canonical run: $(cat "$TMP/skipped.out")"
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
MISMATCH_ROOT="$TMP/mismatched-subject/.kontourai/flow-agents"
|
|
354
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
355
|
+
--artifact-root "$MISMATCH_ROOT" \
|
|
356
|
+
--task-slug arbitrary-session-name \
|
|
357
|
+
--work-item "kontourai/flow-agents#541" \
|
|
358
|
+
--actor builder-entry-mismatch \
|
|
359
|
+
--title "Mismatched assignment subject" \
|
|
360
|
+
--summary "An arbitrary slug is not exact Work Item evidence." \
|
|
361
|
+
--flow-id builder.build >"$TMP/mismatched-subject.out" 2>&1 \
|
|
362
|
+
&& node - "$TMP/mismatched-subject" <<'NODE'
|
|
363
|
+
const fs = require('node:fs');
|
|
364
|
+
const path = require('node:path');
|
|
365
|
+
const project = process.argv[2];
|
|
366
|
+
const session = path.join(project, '.kontourai', 'flow-agents', 'arbitrary-session-name');
|
|
367
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'arbitrary-session-name', 'state.json'), 'utf8'));
|
|
368
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
|
|
369
|
+
if (flowState.subject !== 'kontourai/flow-agents#541' || flowState.current_step !== 'pull-work') process.exit(1);
|
|
370
|
+
if (sidecar.flow_run?.current_step !== 'pull-work') process.exit(1);
|
|
371
|
+
if (fs.existsSync(path.join(session, 'trust.bundle'))) process.exit(1);
|
|
372
|
+
if (!fs.existsSync(path.join(project, '.kontourai', 'flow-agents', 'assignment', 'arbitrary-session-name.json'))) process.exit(1);
|
|
373
|
+
NODE
|
|
374
|
+
then
|
|
375
|
+
pass "assignment subject must canonically match the exact Work Item before pull-work can pass"
|
|
376
|
+
else
|
|
377
|
+
fail "mismatched assignment subject was treated as exact Work Item evidence: $(cat "$TMP/mismatched-subject.out")"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
COLLISION_ROOT="$TMP/collision/.kontourai/flow-agents"
|
|
381
|
+
if ! flow_agents_node "$WRITER" ensure-session \
|
|
382
|
+
--artifact-root "$COLLISION_ROOT" \
|
|
383
|
+
--work-item "owner/a.b#541" \
|
|
384
|
+
--actor builder-entry-collision \
|
|
385
|
+
--title "Collision seed" \
|
|
386
|
+
--summary "Bind the first exact Work Item." >/dev/null 2>&1; then
|
|
387
|
+
fail "collision fixture could not seed the first exact Work Item"
|
|
388
|
+
else
|
|
389
|
+
COLLISION_ASSIGNMENT="$COLLISION_ROOT/assignment/owner-a-b-541.json"
|
|
390
|
+
COLLISION_DIGEST="$(shasum -a 256 "$COLLISION_ASSIGNMENT")"
|
|
391
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
392
|
+
--artifact-root "$COLLISION_ROOT" \
|
|
393
|
+
--work-item "owner/a-b#541" \
|
|
394
|
+
--actor builder-entry-collision \
|
|
395
|
+
--title "Collision attempt" \
|
|
396
|
+
--summary "A colliding slug must not change the exact Work Item." \
|
|
397
|
+
--flow-id builder.build >"$TMP/collision.out" 2>&1; then
|
|
398
|
+
fail "colliding exact Work Item refs should be rejected"
|
|
399
|
+
elif grep -q 'already bound to Work Item "owner/a.b#541", not "owner/a-b#541"' "$TMP/collision.out" \
|
|
400
|
+
&& [[ "$COLLISION_DIGEST" == "$(shasum -a 256 "$COLLISION_ASSIGNMENT")" ]] \
|
|
401
|
+
&& [[ ! -e "$TMP/collision/.kontourai/flow/runs/owner-a-b-541" ]]; then
|
|
402
|
+
pass "colliding slugs cannot change an existing session's exact Work Item binding"
|
|
403
|
+
else
|
|
404
|
+
fail "slug collision mutated ownership or returned the wrong diagnostic: $(cat "$TMP/collision.out")"
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
ASSIGNMENT_ONLY_ROOT="$TMP/assignment-only-collision/.kontourai/flow-agents"
|
|
409
|
+
mkdir -p "$ASSIGNMENT_ONLY_ROOT/assignment"
|
|
410
|
+
cat >"$ASSIGNMENT_ONLY_ROOT/assignment/owner-a-b-542.json" <<'JSON'
|
|
411
|
+
{
|
|
412
|
+
"schema_version": "1.0",
|
|
413
|
+
"role": "AssignmentClaimRecord",
|
|
414
|
+
"subject_id": "owner-a-b-542",
|
|
415
|
+
"actor": { "runtime": "codex", "session_id": "interrupted", "host": "eval-host", "human": null },
|
|
416
|
+
"actor_key": "interrupted",
|
|
417
|
+
"work_item_ref": "owner/a.b#542",
|
|
418
|
+
"claimed_at": "2020-01-01T00:00:00Z",
|
|
419
|
+
"ttl_seconds": 1,
|
|
420
|
+
"branch": "agent/interrupted/owner-a-b-542",
|
|
421
|
+
"artifact_dir": "owner-a-b-542",
|
|
422
|
+
"status": "claimed",
|
|
423
|
+
"audit_trail": []
|
|
424
|
+
}
|
|
425
|
+
JSON
|
|
426
|
+
ASSIGNMENT_ONLY_FILE="$ASSIGNMENT_ONLY_ROOT/assignment/owner-a-b-542.json"
|
|
427
|
+
ASSIGNMENT_ONLY_DIGEST="$(shasum -a 256 "$ASSIGNMENT_ONLY_FILE")"
|
|
428
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
429
|
+
--artifact-root "$ASSIGNMENT_ONLY_ROOT" \
|
|
430
|
+
--work-item "owner/a-b#542" \
|
|
431
|
+
--actor builder-entry-assignment-only-collision \
|
|
432
|
+
--supersede-stale \
|
|
433
|
+
--now "2026-07-10T00:00:00Z" \
|
|
434
|
+
--title "Assignment-only collision" \
|
|
435
|
+
--summary "A crash before state creation must preserve exact assignment identity." \
|
|
436
|
+
--flow-id builder.build >"$TMP/assignment-only-collision.out" 2>&1; then
|
|
437
|
+
fail "assignment-only colliding Work Item should be rejected"
|
|
438
|
+
elif grep -q 'already bound to Work Item "owner/a.b#542", not "owner/a-b#542"' "$TMP/assignment-only-collision.out" \
|
|
439
|
+
&& [[ "$ASSIGNMENT_ONLY_DIGEST" == "$(shasum -a 256 "$ASSIGNMENT_ONLY_FILE")" ]] \
|
|
440
|
+
&& [[ ! -e "$ASSIGNMENT_ONLY_ROOT/owner-a-b-542" ]]; then
|
|
441
|
+
pass "assignment provenance blocks slug collisions before session state exists"
|
|
442
|
+
else
|
|
443
|
+
fail "assignment-only collision mutated provenance or returned the wrong diagnostic: $(cat "$TMP/assignment-only-collision.out")"
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
PREEXISTING_ROOT="$TMP/preexisting/.kontourai/flow-agents"
|
|
447
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
448
|
+
--artifact-root "$PREEXISTING_ROOT" \
|
|
449
|
+
--task-slug preexisting-selection \
|
|
450
|
+
--actor builder-entry-preexisting \
|
|
451
|
+
--title "Preexisting selection" \
|
|
452
|
+
--summary "Seed an assignment before the canonical Builder run." >/dev/null 2>&1 \
|
|
453
|
+
&& flow_agents_node "$WRITER" ensure-session \
|
|
454
|
+
--artifact-root "$PREEXISTING_ROOT" \
|
|
455
|
+
--task-slug preexisting-selection \
|
|
456
|
+
--actor builder-entry-preexisting \
|
|
457
|
+
--title "Preexisting selection" \
|
|
458
|
+
--summary "An older self-held assignment is not new selection evidence." \
|
|
459
|
+
--flow-id builder.build >"$TMP/preexisting.out" 2>&1 \
|
|
460
|
+
&& node - "$TMP/preexisting" <<'NODE'
|
|
461
|
+
const fs = require('node:fs');
|
|
462
|
+
const path = require('node:path');
|
|
463
|
+
const project = process.argv[2];
|
|
464
|
+
const session = path.join(project, '.kontourai', 'flow-agents', 'preexisting-selection');
|
|
465
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'preexisting-selection', 'state.json'), 'utf8'));
|
|
466
|
+
if (flowState.current_step !== 'pull-work') process.exit(1);
|
|
467
|
+
if (fs.existsSync(path.join(session, 'trust.bundle'))) process.exit(1);
|
|
468
|
+
NODE
|
|
469
|
+
then
|
|
470
|
+
pass "preexisting self-held assignments do not retroactively satisfy pull-work"
|
|
255
471
|
else
|
|
256
|
-
fail "
|
|
472
|
+
fail "preexisting assignment was treated as current selection evidence: $(cat "$TMP/preexisting.out")"
|
|
257
473
|
fi
|
|
258
474
|
|
|
259
475
|
if FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000 \
|
|
@@ -881,6 +881,49 @@ else
|
|
|
881
881
|
_fail "Codex full install lost Builder specialist role routing"
|
|
882
882
|
fi
|
|
883
883
|
|
|
884
|
+
echo ""
|
|
885
|
+
echo "--- Packed Package Builder Entry ---"
|
|
886
|
+
PACKAGE_CONSUMER="$TMPDIR_EVAL/package-consumer"
|
|
887
|
+
PACKAGE_PROJECT="$TMPDIR_EVAL/package-project"
|
|
888
|
+
PACKAGE_AMBIENT="$TMPDIR_EVAL/package-ambient"
|
|
889
|
+
mkdir -p "$PACKAGE_CONSUMER" "$PACKAGE_PROJECT/.kontourai/flow-agents" "$PACKAGE_AMBIENT/kits/builder/flows"
|
|
890
|
+
cat >"$PACKAGE_AMBIENT/kits/builder/flows/build.flow.json" <<'JSON'
|
|
891
|
+
{
|
|
892
|
+
"id": "builder.build",
|
|
893
|
+
"version": "poison-ambient-cwd",
|
|
894
|
+
"steps": [{ "id": "ambient-poison", "next": null }],
|
|
895
|
+
"gates": {}
|
|
896
|
+
}
|
|
897
|
+
JSON
|
|
898
|
+
PACKAGE_PACK_LOG="$TMPDIR_EVAL/package-pack.log"
|
|
899
|
+
if (cd "$ROOT_DIR" && npm pack --silent --pack-destination "$TMPDIR_EVAL" >"$PACKAGE_PACK_LOG") \
|
|
900
|
+
&& PACKAGE_TARBALL="$(find "$TMPDIR_EVAL" -maxdepth 1 -type f -name 'kontourai-flow-agents-*.tgz' -print -quit)" \
|
|
901
|
+
&& [[ -n "$PACKAGE_TARBALL" ]] \
|
|
902
|
+
&& npm install --silent --no-audit --no-fund --ignore-scripts --prefix "$PACKAGE_CONSUMER" "$PACKAGE_TARBALL" \
|
|
903
|
+
&& (cd "$PACKAGE_AMBIENT" && node "$PACKAGE_CONSUMER/node_modules/@kontourai/flow-agents/build/src/cli/workflow-sidecar.js" ensure-session \
|
|
904
|
+
--artifact-root "$PACKAGE_PROJECT/.kontourai/flow-agents" \
|
|
905
|
+
--task-slug packed-builder-entry \
|
|
906
|
+
--actor packed-package-consumer \
|
|
907
|
+
--title "Packed Builder entry" \
|
|
908
|
+
--summary "Installed package should project pickup-probe." \
|
|
909
|
+
--flow-id builder.build >/dev/null 2>&1) \
|
|
910
|
+
&& node - "$PACKAGE_PROJECT" <<'NODE'
|
|
911
|
+
const fs = require('node:fs');
|
|
912
|
+
const path = require('node:path');
|
|
913
|
+
const project = process.argv[2];
|
|
914
|
+
const state = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'packed-builder-entry', 'state.json'), 'utf8'));
|
|
915
|
+
const flow = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'packed-builder-entry', 'state.json'), 'utf8'));
|
|
916
|
+
const bundle = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'packed-builder-entry', 'trust.bundle'), 'utf8'));
|
|
917
|
+
if (flow.current_step !== 'design-probe' || state.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
918
|
+
if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
919
|
+
if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) process.exit(1);
|
|
920
|
+
NODE
|
|
921
|
+
then
|
|
922
|
+
_pass "packed npm consumer ignores unrelated ambient Flow definitions and projects pickup-probe"
|
|
923
|
+
else
|
|
924
|
+
_fail "packed npm consumer did not execute canonical Builder entry"
|
|
925
|
+
fi
|
|
926
|
+
|
|
884
927
|
if [[ -d "$CODEX_FULL_DEST/.codex/skills/plan-work" && -d "$CODEX_FULL_DEST/.codex/skills/deliver" && -d "$CODEX_FULL_DEST/.codex/skills/agentic-engineering" ]]; then
|
|
885
928
|
_pass "Codex full install ships kit-skills and standalone skills together"
|
|
886
929
|
else
|
|
@@ -377,6 +377,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
377
377
|
--task-slug gate-actor-a \
|
|
378
378
|
--actor eval-actor-a-session \
|
|
379
379
|
--flow-id builder.build \
|
|
380
|
+
--skip-ownership-guard \
|
|
380
381
|
--source-request "Actor A is active on a FlowDefinition-driven session." \
|
|
381
382
|
--summary "Actor A, builder.build, pull-work step." \
|
|
382
383
|
--timestamp "2026-07-01T00:00:00Z" \
|
|
@@ -258,6 +258,7 @@ if flow_agents_node "$WRITER" ensure-session \
|
|
|
258
258
|
--artifact-root "$SESSION_ROOT" \
|
|
259
259
|
--task-slug dual-emit-test \
|
|
260
260
|
--flow-id builder.build \
|
|
261
|
+
--skip-ownership-guard \
|
|
261
262
|
--title "Declared-Only Test" \
|
|
262
263
|
--summary "Test declared-only emit for ADR 0016 P-d." \
|
|
263
264
|
--criterion "Tests pass" \
|
|
@@ -348,6 +349,7 @@ if flow_agents_node "$WRITER" ensure-session \
|
|
|
348
349
|
--artifact-root "$SESSION_ROOT" \
|
|
349
350
|
--task-slug dual-emit-policy \
|
|
350
351
|
--flow-id builder.build \
|
|
352
|
+
--skip-ownership-guard \
|
|
351
353
|
--title "Declared Policy Test" \
|
|
352
354
|
--summary "Test declared-only policy emit for ADR 0016 P-d." \
|
|
353
355
|
--criterion "Policy passes" \
|
|
@@ -51,6 +51,7 @@ mkdir -p "$MAIN_AROOT"
|
|
|
51
51
|
if flow_agents_node "$WRITER" ensure-session \
|
|
52
52
|
--artifact-root "$MAIN_AROOT" \
|
|
53
53
|
--task-slug "$SLUG" \
|
|
54
|
+
--actor activation-test-actor \
|
|
54
55
|
--title "Step 1 activation test" \
|
|
55
56
|
--summary "Test that --flow-id builder.build activates the FlowDefinition-driven path." \
|
|
56
57
|
--criterion "All gates produce declared claims" \
|
|
@@ -65,12 +66,14 @@ node -e "
|
|
|
65
66
|
const fs = require('fs');
|
|
66
67
|
const c = JSON.parse(fs.readFileSync('$MAIN_AROOT/current.json', 'utf8'));
|
|
67
68
|
const flow = JSON.parse(fs.readFileSync('$TMP/main-project/.kontourai/flow/runs/$SLUG/state.json', 'utf8'));
|
|
69
|
+
const bundle = JSON.parse(fs.readFileSync('$SESSION_DIR/trust.bundle', 'utf8'));
|
|
68
70
|
if (c.active_flow_id !== 'builder.build') throw new Error('expected active_flow_id=builder.build, got ' + c.active_flow_id);
|
|
69
|
-
if (
|
|
70
|
-
if (flow.status !== 'active' || flow.current_step !== '
|
|
71
|
+
if (c.active_step_id !== 'design-probe') throw new Error('expected active_step_id=design-probe, got ' + c.active_step_id);
|
|
72
|
+
if (flow.status !== 'active' || flow.current_step !== 'design-probe') throw new Error('canonical Flow did not advance through trusted selection: ' + JSON.stringify(flow));
|
|
73
|
+
if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) throw new Error('missing verified selected-work claim');
|
|
71
74
|
console.log('current.json: active_flow_id=' + c.active_flow_id + ' active_step_id=' + c.active_step_id);
|
|
72
75
|
" 2>&1 \
|
|
73
|
-
&& _pass "ensure-session
|
|
76
|
+
&& _pass "ensure-session records trusted selection and projects the canonical Flow run at design-probe" \
|
|
74
77
|
|| _fail "ensure-session did not create and project the canonical Flow run"
|
|
75
78
|
|
|
76
79
|
# ─── TEST 2: advance-state sets active_step_id via phase_map ─────────────────
|
|
@@ -15,10 +15,10 @@ _pass() { echo " ✓ $1"; }
|
|
|
15
15
|
_fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
16
16
|
|
|
17
17
|
REPO="$TMPDIR_EVAL/repo"
|
|
18
|
-
mkdir -p "$REPO/.flow-agents/stuck"
|
|
18
|
+
mkdir -p "$REPO/.kontourai/flow-agents/stuck"
|
|
19
19
|
printf '# Test Repo\n' > "$REPO/AGENTS.md"
|
|
20
20
|
printf '# Stuck\n\nbranch: main\nstatus: executing\ntype: deliver\n\n## Plan\n\nTBD.\n' \
|
|
21
|
-
> "$REPO/.flow-agents/stuck/stuck--deliver.md"
|
|
21
|
+
> "$REPO/.kontourai/flow-agents/stuck/stuck--deliver.md"
|
|
22
22
|
|
|
23
23
|
PAYLOAD="{\"hook_event_name\":\"Stop\",\"cwd\":\"$REPO\"}"
|
|
24
24
|
|
|
@@ -54,7 +54,7 @@ c4=$(run_block "$TMPDIR_EVAL/b4.err")
|
|
|
54
54
|
printf '%s' "$PAYLOAD" | FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=3 node "$ROOT/scripts/hooks/stop-goal-fit.js" >/dev/null 2>/dev/null
|
|
55
55
|
# mutate the artifact so the warning set differs
|
|
56
56
|
printf '# Stuck\n\nbranch: main\nstatus: verifying\ntype: deliver\n\n## Plan\n\nDifferent.\n' \
|
|
57
|
-
> "$REPO/.flow-agents/stuck/stuck--deliver.md"
|
|
57
|
+
> "$REPO/.kontourai/flow-agents/stuck/stuck--deliver.md"
|
|
58
58
|
cd=$(run_block "$TMPDIR_EVAL/bd.err")
|
|
59
59
|
[[ "$cd" -eq 2 ]] && rg -q 'Stop blocked .* \(block 1; after 3 identical blocks' "$TMPDIR_EVAL/bd.err" \
|
|
60
60
|
&& _pass "changed goal-fit gap resets the streak to 1/3" \
|
|
@@ -129,6 +129,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
129
129
|
--title "Ensure Default Step" \
|
|
130
130
|
--summary "Test ensure-session default step." \
|
|
131
131
|
--flow-id builder.build \
|
|
132
|
+
--skip-ownership-guard \
|
|
132
133
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
133
134
|
|
|
134
135
|
node -e "
|
|
@@ -154,6 +155,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
154
155
|
--title "Gate Claim Test" \
|
|
155
156
|
--summary "Test gate claim producer." \
|
|
156
157
|
--flow-id builder.build \
|
|
158
|
+
--skip-ownership-guard \
|
|
157
159
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
158
160
|
|
|
159
161
|
flow_agents_node "workflow-sidecar" init-plan "$CLAIM_ROOT/gate-claim/gate-claim--deliver.md" \
|
|
@@ -261,6 +263,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
261
263
|
--title "Tamper Test" \
|
|
262
264
|
--summary "Testing tamper detection." \
|
|
263
265
|
--flow-id builder.build \
|
|
266
|
+
--skip-ownership-guard \
|
|
264
267
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
265
268
|
|
|
266
269
|
flow_agents_node "workflow-sidecar" init-plan "$T_DIR/.kontourai/flow-agents/tamper/tamper--deliver.md" \
|
|
@@ -364,6 +367,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
|
|
|
364
367
|
--title "Clean Test" \
|
|
365
368
|
--summary "Testing clean gate claim." \
|
|
366
369
|
--flow-id builder.build \
|
|
370
|
+
--skip-ownership-guard \
|
|
367
371
|
--timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
|
|
368
372
|
|
|
369
373
|
flow_agents_node "workflow-sidecar" init-plan "$C_DIR/.kontourai/flow-agents/clean/clean--deliver.md" \
|
|
@@ -4228,6 +4228,7 @@ flow_agents_node "$WRITER" ensure-session \
|
|
|
4228
4228
|
--task-slug "$UNRESOLVABLE_SLUG" \
|
|
4229
4229
|
--actor unresolvable-actor \
|
|
4230
4230
|
--flow-id builder.build \
|
|
4231
|
+
--skip-ownership-guard \
|
|
4231
4232
|
--title "Unresolvable FlowDefinition regression" \
|
|
4232
4233
|
--source-request "Regression: an unresolvable FlowDefinition must die with the dedicated cannot-be-loaded message." \
|
|
4233
4234
|
--summary "Seed session with a REAL flow so a real stamped gate claim can be recorded." \
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -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;
|
|
@@ -218,16 +219,50 @@ export function assignmentFilePath(artifactRoot: string, subjectId: string): str
|
|
|
218
219
|
return path.join(artifactRoot, "assignment", `${sanitized}.json`);
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
function localAssignmentDir(artifactRoot: string, create: boolean): string | null {
|
|
223
|
+
const dir = path.join(artifactRoot, "assignment");
|
|
224
|
+
if (create) fs.mkdirSync(dir, { recursive: true });
|
|
225
|
+
let stat: fs.Stats;
|
|
226
|
+
try {
|
|
227
|
+
stat = fs.lstatSync(dir);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (!create && (error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
233
|
+
throw new Error(`assignment directory must be a real directory, not a symlink: ${dir}`);
|
|
234
|
+
}
|
|
235
|
+
const realRoot = fs.realpathSync(artifactRoot);
|
|
236
|
+
if (fs.realpathSync(dir) !== path.join(realRoot, "assignment")) {
|
|
237
|
+
throw new Error(`assignment directory escapes the artifact root: ${dir}`);
|
|
238
|
+
}
|
|
239
|
+
return dir;
|
|
240
|
+
}
|
|
241
|
+
|
|
221
242
|
export function readLocalRecord(artifactRoot: string, subjectId: string): AssignmentClaimRecord | null {
|
|
243
|
+
if (!localAssignmentDir(artifactRoot, false)) return null;
|
|
222
244
|
const file = assignmentFilePath(artifactRoot, subjectId);
|
|
223
|
-
|
|
245
|
+
let stat: fs.Stats;
|
|
246
|
+
try {
|
|
247
|
+
stat = fs.lstatSync(file);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
253
|
+
throw new Error(`assignment record must be a regular file, not a symlink: ${file}`);
|
|
254
|
+
}
|
|
224
255
|
let data: unknown;
|
|
256
|
+
let descriptor: number | null = null;
|
|
225
257
|
try {
|
|
226
|
-
|
|
258
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
259
|
+
data = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
227
260
|
} catch (error) {
|
|
228
261
|
// Fail loud: a corrupt claim record must never be silently treated as "no claim" — that
|
|
229
262
|
// would be a fail-open path that could let a second claim silently overwrite a real one.
|
|
230
263
|
throw new Error(`assignment record is corrupt, refusing to proceed: ${file}: ${(error as Error).message}`);
|
|
264
|
+
} finally {
|
|
265
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
231
266
|
}
|
|
232
267
|
if (typeof data !== "object" || data === null) throw new Error(`assignment record is not an object: ${file}`);
|
|
233
268
|
const record = data as AssignmentClaimRecord;
|
|
@@ -239,7 +274,7 @@ export function writeLocalRecord(artifactRoot: string, subjectId: string, record
|
|
|
239
274
|
// writeJson throws on any mkdir/writeFileSync failure; that error is intentionally allowed to
|
|
240
275
|
// propagate to main()'s top-level try/catch and exit non-zero. Durable writes must fail loud,
|
|
241
276
|
// never fail open (artifact-contract.md).
|
|
242
|
-
|
|
277
|
+
atomicWriteJson(artifactRoot, assignmentFilePath(artifactRoot, subjectId), record);
|
|
243
278
|
}
|
|
244
279
|
|
|
245
280
|
/**
|
|
@@ -255,8 +290,7 @@ function sleepSync(ms: number): void {
|
|
|
255
290
|
}
|
|
256
291
|
|
|
257
292
|
function subjectLockDir(artifactRoot: string, subjectId: string): string {
|
|
258
|
-
const assignmentDir =
|
|
259
|
-
fs.mkdirSync(assignmentDir, { recursive: true });
|
|
293
|
+
const assignmentDir = localAssignmentDir(artifactRoot, true)!;
|
|
260
294
|
const sanitized = loadActorIdentityHelper().sanitizeSegment(subjectId);
|
|
261
295
|
return path.join(assignmentDir, `.${sanitized}.lockdir`);
|
|
262
296
|
}
|
|
@@ -310,11 +344,19 @@ export function withSubjectLock<T>(artifactRoot: string, subjectId: string, body
|
|
|
310
344
|
sleepSync(20);
|
|
311
345
|
}
|
|
312
346
|
}
|
|
347
|
+
const release = (): void => fs.rmSync(lockDir, { recursive: true, force: true });
|
|
348
|
+
let result: T;
|
|
313
349
|
try {
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
350
|
+
result = body();
|
|
351
|
+
} catch (error) {
|
|
352
|
+
release();
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
if (result && typeof (result as { then?: unknown }).then === "function") {
|
|
356
|
+
return Promise.resolve(result).finally(release) as T;
|
|
317
357
|
}
|
|
358
|
+
release();
|
|
359
|
+
return result;
|
|
318
360
|
}
|
|
319
361
|
|
|
320
362
|
/**
|
|
@@ -512,7 +554,7 @@ export function performLocalClaim(
|
|
|
512
554
|
artifactRoot: string,
|
|
513
555
|
subjectId: string,
|
|
514
556
|
actor: ActorStruct,
|
|
515
|
-
opts: { ttlSeconds: number; branch: string; artifactDir: string; reason?: string; actorKey?: string },
|
|
557
|
+
opts: { ttlSeconds: number; branch: string; artifactDir: string; reason?: string; actorKey?: string; workItemRef?: string },
|
|
516
558
|
): AssignmentClaimRecord {
|
|
517
559
|
const helper = loadActorIdentityHelper();
|
|
518
560
|
const reason = opts.reason ?? "claim";
|
|
@@ -538,6 +580,7 @@ export function performLocalClaim(
|
|
|
538
580
|
subject_id: subjectId,
|
|
539
581
|
actor,
|
|
540
582
|
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
583
|
+
...((opts.workItemRef ?? existing?.work_item_ref) ? { work_item_ref: opts.workItemRef ?? existing?.work_item_ref } : {}),
|
|
541
584
|
claimed_at: isoNow(),
|
|
542
585
|
ttl_seconds: opts.ttlSeconds,
|
|
543
586
|
branch: opts.branch,
|
|
@@ -707,7 +750,7 @@ export function performLocalSupersede(
|
|
|
707
750
|
subjectId: string,
|
|
708
751
|
fromActor: ActorStruct,
|
|
709
752
|
toActor: ActorStruct,
|
|
710
|
-
opts: { ttlSeconds?: number; branch?: string; artifactDir?: string; reason?: string; actorKey?: string } = {},
|
|
753
|
+
opts: { ttlSeconds?: number; branch?: string; artifactDir?: string; reason?: string; actorKey?: string; workItemRef?: string } = {},
|
|
711
754
|
): AssignmentClaimRecord {
|
|
712
755
|
const helper = loadActorIdentityHelper();
|
|
713
756
|
const reason = opts.reason ?? "supersede";
|
|
@@ -732,6 +775,7 @@ export function performLocalSupersede(
|
|
|
732
775
|
subject_id: subjectId,
|
|
733
776
|
actor: toActor,
|
|
734
777
|
...(opts.actorKey ? { actor_key: opts.actorKey } : {}),
|
|
778
|
+
...((opts.workItemRef ?? existing.work_item_ref) ? { work_item_ref: opts.workItemRef ?? existing.work_item_ref } : {}),
|
|
735
779
|
claimed_at: isoNow(),
|
|
736
780
|
ttl_seconds: ttlSeconds,
|
|
737
781
|
branch: opts.branch ?? existing.branch,
|