@kontourai/flow-agents 3.6.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/src/builder-flow-run-adapter.d.ts +22 -2
  3. package/build/src/builder-flow-run-adapter.js +93 -28
  4. package/build/src/builder-flow-runtime.d.ts +8 -3
  5. package/build/src/builder-flow-runtime.js +407 -51
  6. package/build/src/cli/assignment-provider.d.ts +4 -7
  7. package/build/src/cli/assignment-provider.js +80 -14
  8. package/build/src/cli/builder-run.js +14 -18
  9. package/build/src/cli/workflow-sidecar.d.ts +28 -4
  10. package/build/src/cli/workflow-sidecar.js +825 -103
  11. package/build/src/cli/workflow.js +301 -43
  12. package/build/src/flow-kit/validate.d.ts +17 -0
  13. package/build/src/flow-kit/validate.js +340 -2
  14. package/build/src/index.js +5 -5
  15. package/build/src/lib/observed-command.d.ts +7 -0
  16. package/build/src/lib/observed-command.js +100 -0
  17. package/build/src/tools/build-universal-bundles.js +53 -3
  18. package/build/src/tools/generate-context-map.js +5 -3
  19. package/context/scripts/hooks/lib/kit-catalog.js +1 -1
  20. package/context/scripts/hooks/stop-goal-fit.js +78 -9
  21. package/docs/context-map.md +22 -20
  22. package/docs/developer-architecture.md +1 -1
  23. package/docs/getting-started.md +9 -1
  24. package/docs/public-workflow-cli.md +76 -7
  25. package/docs/skills-map.md +51 -27
  26. package/docs/spec/builder-flow-runtime.md +75 -40
  27. package/docs/workflow-usage-guide.md +109 -42
  28. package/evals/fixtures/hook-influence/cases.json +2 -2
  29. package/evals/integration/test_builder_entry_enforcement.sh +52 -41
  30. package/evals/integration/test_builder_step_producers.sh +330 -6
  31. package/evals/integration/test_bundle_install.sh +318 -65
  32. package/evals/integration/test_bundle_lifecycle.sh +4 -6
  33. package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
  34. package/evals/integration/test_current_json_per_actor.sh +12 -0
  35. package/evals/integration/test_dual_emit_flow_step.sh +62 -43
  36. package/evals/integration/test_flowdef_session_activation.sh +145 -25
  37. package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
  38. package/evals/integration/test_gate_lockdown.sh +3 -5
  39. package/evals/integration/test_goal_fit_hook.sh +60 -2
  40. package/evals/integration/test_install_merge.sh +18 -8
  41. package/evals/integration/test_liveness_verdict.sh +14 -17
  42. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
  43. package/evals/integration/test_public_workflow_cli.sh +334 -14
  44. package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
  45. package/evals/integration/test_sidecar_field_preservation.sh +36 -11
  46. package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
  47. package/evals/integration/test_workflow_steering_hook.sh +15 -38
  48. package/evals/run.sh +2 -0
  49. package/evals/static/test_builder_skill_coherence.sh +247 -0
  50. package/evals/static/test_library_exports.sh +5 -2
  51. package/evals/static/test_universal_bundles.sh +44 -1
  52. package/evals/static/test_workflow_skills.sh +13 -325
  53. package/kits/builder/flows/build.flow.json +22 -0
  54. package/kits/builder/flows/shape.flow.json +9 -9
  55. package/kits/builder/kit.json +70 -16
  56. package/kits/builder/skills/builder-shape/SKILL.md +75 -75
  57. package/kits/builder/skills/continue-work/SKILL.md +45 -106
  58. package/kits/builder/skills/deliver/SKILL.md +96 -442
  59. package/kits/builder/skills/design-probe/SKILL.md +40 -139
  60. package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
  61. package/kits/builder/skills/execute-plan/SKILL.md +54 -125
  62. package/kits/builder/skills/fix-bug/SKILL.md +42 -132
  63. package/kits/builder/skills/gate-review/SKILL.md +60 -211
  64. package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
  65. package/kits/builder/skills/learning-review/SKILL.md +63 -170
  66. package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
  67. package/kits/builder/skills/plan-work/SKILL.md +51 -185
  68. package/kits/builder/skills/pull-work/SKILL.md +136 -485
  69. package/kits/builder/skills/release-readiness/SKILL.md +66 -107
  70. package/kits/builder/skills/review-work/SKILL.md +89 -176
  71. package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
  72. package/kits/builder/skills/verify-work/SKILL.md +101 -113
  73. package/package.json +2 -2
  74. package/packaging/README.md +1 -1
  75. package/scripts/README.md +1 -1
  76. package/scripts/hooks/lib/kit-catalog.js +1 -1
  77. package/scripts/hooks/stop-goal-fit.js +78 -9
  78. package/scripts/install-codex-home.sh +63 -23
  79. package/scripts/install-owned-files.js +18 -2
  80. package/src/builder-flow-run-adapter.ts +118 -32
  81. package/src/builder-flow-runtime.ts +426 -64
  82. package/src/cli/assignment-provider-lock.test.mjs +83 -0
  83. package/src/cli/assignment-provider.ts +75 -14
  84. package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
  85. package/src/cli/builder-flow-runtime.test.mjs +436 -18
  86. package/src/cli/builder-run.ts +3 -9
  87. package/src/cli/kit-metadata-security.test.mjs +232 -6
  88. package/src/cli/public-api.test.mjs +15 -0
  89. package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
  90. package/src/cli/workflow-sidecar.ts +746 -103
  91. package/src/cli/workflow.ts +288 -41
  92. package/src/flow-kit/validate.ts +320 -2
  93. package/src/index.ts +6 -5
  94. package/src/lib/observed-command.ts +96 -0
  95. package/src/tools/build-universal-bundles.ts +51 -3
  96. package/src/tools/generate-context-map.ts +5 -3
@@ -18,23 +18,111 @@ VERSION="$(node -p "require('./package.json').version")"
18
18
  CONSUMER="$TMP/consumer"
19
19
  ARTIFACT_ROOT="$CONSUMER/.kontourai/flow-agents"
20
20
  mkdir -p "$CONSUMER"
21
+ mkdir -p "$CONSUMER/checks"
22
+ printf '#!/usr/bin/env bash\nset -eu\ntest -f "$1"\nprintf "1..1\\nok 1 - session exists\\n"\n' > "$CONSUMER/checks/check-public-workflow.sh"
23
+ chmod +x "$CONSUMER/checks/check-public-workflow.sh"
24
+ printf '#!/usr/bin/env bash\nset -eu\ntouch "$1"\nsleep 1\n' > "$CONSUMER/checks/check-command-lock.sh"
25
+ chmod +x "$CONSUMER/checks/check-command-lock.sh"
26
+ printf '#!/usr/bin/env bash\nset -eu\ntest -f "$1"\nprintf "run\\n" >> "$2"\nprintf "1..1\\nok 1 - session exists\\n"\n' > "$CONSUMER/checks/check-multi-command.sh"
27
+ chmod +x "$CONSUMER/checks/check-multi-command.sh"
28
+ printf '#!/usr/bin/env bash\nset -eu\ntrap "" TERM\n( trap "" TERM; sleep 5; touch "$2" ) &\nchild=$!\nprintf "%s\\n" "$child" > "$1"\nwait "$child"\ntouch "$2"\n' > "$CONSUMER/checks/check-command-timeout.sh"
29
+ chmod +x "$CONSUMER/checks/check-command-timeout.sh"
30
+ printf '#!/usr/bin/env bash\nset -eu\n( trap "" TERM; while ! sleep 5; do :; done; touch "$2" ) &\nprintf "%s\\n" "$!" > "$1"\n' > "$CONSUMER/checks/check-success-background.sh"
31
+ chmod +x "$CONSUMER/checks/check-success-background.sh"
21
32
 
22
33
  run_candidate() {
23
34
  (cd "$CONSUMER" && CODEX_SESSION_ID=public-workflow-eval npx --yes --package="file:$TARBALL" flow-agents workflow "$@")
24
35
  }
25
36
 
37
+ run_candidate_as() {
38
+ local actor="$1"
39
+ shift
40
+ (cd "$CONSUMER" && CODEX_SESSION_ID="$actor" npx --yes --package="file:$TARBALL" flow-agents workflow "$@")
41
+ }
42
+
43
+ snapshot_tree() {
44
+ local root="$1"
45
+ find "$root" -type f -print0 | sort -z | xargs -0 shasum -a 256
46
+ }
47
+
26
48
  PRIMARY_HELP="$(cd "$CONSUMER" && npx --yes --package="file:$TARBALL" flow-agents --help)"
27
49
  WORKFLOW_HELP="$(run_candidate --help)"
28
50
  [[ "$PRIMARY_HELP" == *"workflow"* && "$WORKFLOW_HELP" != *"workflow-sidecar"* && "$WORKFLOW_HELP" != *"npm run workflow:sidecar"* ]] || fail "public help exposes internal writer terminology or omits workflow"
29
51
  pass "primary help exposes the public workflow command without internal writer terminology"
30
52
 
31
- run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#101 --summary "Release fixture" >/dev/null
53
+ seed_pull_work() {
54
+ local work_item="$1"
55
+ local slug
56
+ slug="$(printf '%s' "$work_item" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')"
57
+ mkdir -p "$ARTIFACT_ROOT/$slug"
58
+ printf 'Selected Work Item: %s\n' "$work_item" >"$ARTIFACT_ROOT/$slug/$slug--pull-work.md"
59
+ }
60
+
61
+ seed_pull_work acme/widgets#101
62
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#101 --assignment-provider local-file --summary "Release fixture" >/dev/null
32
63
  RELEASE_SESSION="$ARTIFACT_ROOT/acme-widgets-101"
33
64
  [[ -f "$RELEASE_SESSION/state.json" ]] || fail "packed start did not create a session"
34
65
  [[ ! -e "$CONSUMER/package.json" ]] || fail "consumer unexpectedly gained package.json"
35
66
  pass "packed start works in a non-Node consumer"
67
+ seed_pull_work provider:work-item-123
68
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item provider:work-item-123 --assignment-provider local-file --summary "Provider-neutral fixture" >/dev/null
69
+ PROVIDER_STATUS="$(run_candidate status --session-dir "$ARTIFACT_ROOT/provider-work-item-123" --json)"
70
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.definition_id!=="builder.build"||r.current_step!=="design-probe")process.exit(1)' "$PROVIDER_STATUS" || fail "provider-neutral Work Item did not start canonically"
71
+ pass "documented provider-neutral Work Item refs start without GitHub identity inference"
72
+ seed_pull_work provider:externally-owned-456
73
+ PROVIDER_STATE="$CONSUMER/provider-assignment-state.json"
74
+ node - "$PROVIDER_STATE" "$ARTIFACT_ROOT/assignment/acme-widgets-101.json" <<'NODE'
75
+ const fs = require('node:fs');
76
+ const local = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
77
+ const subject = 'provider-externally-owned-456';
78
+ const record = { ...local, subject_id: subject, work_item_ref: 'provider:externally-owned-456' };
79
+ fs.writeFileSync(process.argv[2], `${JSON.stringify({ role: 'AssignmentStatus', provider: 'example-provider', assignment: { subject_id: subject, provider: 'example-provider', assignee: local.actor_key, record }, effective: { effective_state: 'held', reason: 'self_is_holder', holder: { actor: local.actor_key } } }, null, 2)}\n`);
80
+ NODE
81
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item provider:externally-owned-456 --assignment-provider example-provider --effective-state-json "$PROVIDER_STATE" --summary "Externally assigned fixture" >/dev/null
82
+ EXTERNAL_SESSION="$ARTIFACT_ROOT/provider-externally-owned-456"
83
+ node - "$ARTIFACT_ROOT/assignment/provider-externally-owned-456.json" "$EXTERNAL_SESSION/assignment-provider-state.json" "$EXTERNAL_SESSION/trust.bundle" <<'NODE'
84
+ const fs = require('node:fs');
85
+ const assignment = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
86
+ const provider = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
87
+ const bundle = JSON.parse(fs.readFileSync(process.argv[4], 'utf8'));
88
+ const selected = bundle.claims.find((claim) => claim.metadata?.gate_claim?.expectation_id === 'selected-work');
89
+ if (assignment.work_item_ref !== 'provider:externally-owned-456' || provider.effective?.reason !== 'self_is_holder' || !selected?.metadata?.artifact_refs?.some((ref) => ref.file?.endsWith('assignment-provider-state.json'))) process.exit(1);
90
+ NODE
91
+ pass "provider-confirmed ownership is retained as evidence and mirrored into a local runtime lease"
92
+ seed_pull_work provider:missing-provider
36
93
  set +e
37
- UNSAFE_START="$(run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#103 --skip-ownership-guard 2>&1)"
94
+ MISSING_PROVIDER="$(run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item provider:missing-provider --summary "Missing provider fixture" 2>&1)"
95
+ MISSING_PROVIDER_RC=$?
96
+ set -e
97
+ [[ "$MISSING_PROVIDER_RC" -ne 0 && "$MISSING_PROVIDER" == *"requires --assignment-provider"* && ! -e "$ARTIFACT_ROOT/provider-missing-provider/state.json" ]] || fail "public start inferred an assignment provider or mutated before rejecting it"
98
+ pass "public start requires explicit assignment-provider resolution before mutation"
99
+ set +e
100
+ MISSING_PULL_WORK="$(run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#105 --assignment-provider local-file --summary "Missing selection evidence" 2>&1)"
101
+ MISSING_PULL_WORK_RC=$?
102
+ set -e
103
+ [[ "$MISSING_PULL_WORK_RC" -ne 0 && "$MISSING_PULL_WORK" == *"requires concrete pull-work selection evidence"* && ! -e "$ARTIFACT_ROOT/acme-widgets-105/state.json" ]] || fail "public start produced selected-work without a concrete pull-work report"
104
+ pass "public start requires session-local pull-work selection evidence before auto-producing selected-work"
105
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.shape --task-slug shape-fixture --summary "Shape fixture" >/dev/null
106
+ SHAPE_SESSION="$ARTIFACT_ROOT/shape-fixture"
107
+ SHAPE_STATUS="$(run_candidate status --session-dir "$SHAPE_SESSION" --json)"
108
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.definition_id!=="builder.shape"||r.current_step!=="shape")process.exit(1)' "$SHAPE_STATUS" || fail "shape status did not resolve canonical Flow run"
109
+ node - "$SHAPE_SESSION/work-item.json" <<'NODE'
110
+ const fs = require('node:fs');
111
+ const record = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
112
+ if (record.id !== 'shape-fixture' || record.source_provider?.kind !== 'local') process.exit(1);
113
+ NODE
114
+ run_candidate evidence --session-dir "$SHAPE_SESSION" --expectation shaped-problem --status not_verified --summary "Shape fixture remains intentionally unverified." --json >/dev/null
115
+ pass "shape starts as an explicit local Work Item and records public evidence through Flow"
116
+ SHAPE_RETRY_BEFORE="$(snapshot_tree "$ARTIFACT_ROOT/shape-fixture")"
117
+ set +e
118
+ SHAPE_TO_BUILD="$(run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item local:shape-fixture --task-slug shape-fixture --assignment-provider local-file --summary "Invalid shape retry" 2>&1)"
119
+ SHAPE_TO_BUILD_RC=$?
120
+ set -e
121
+ SHAPE_RETRY_AFTER="$(snapshot_tree "$ARTIFACT_ROOT/shape-fixture")"
122
+ [[ "$SHAPE_TO_BUILD_RC" -ne 0 && "$SHAPE_TO_BUILD" == *"local shape sessions are not build retries"* && "$SHAPE_RETRY_BEFORE" == "$SHAPE_RETRY_AFTER" ]] || fail "public local shape-to-build retry did not fail clearly before mutation"
123
+ pass "public start rejects local shape-to-build retries; provider Work Items are the build handoff"
124
+ set +e
125
+ UNSAFE_START="$(run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#103 --assignment-provider local-file --skip-ownership-guard 2>&1)"
38
126
  UNSAFE_START_RC=$?
39
127
  set -e
40
128
  [[ "$UNSAFE_START_RC" -ne 0 && "$UNSAFE_START" == *"does not support --skip-ownership-guard"* && ! -e "$ARTIFACT_ROOT/acme-widgets-103" ]] || fail "public start accepted an internal ownership bypass"
@@ -42,7 +130,8 @@ pass "public start rejects internal authority flags before mutation"
42
130
 
43
131
  LOCAL_RETRY_PROJECT="$TMP/local-retry-project"
44
132
  LOCAL_RETRY_ROOT="$LOCAL_RETRY_PROJECT/.kontourai/flow-agents"
45
- mkdir -p "$LOCAL_RETRY_PROJECT/.kontourai"
133
+ mkdir -p "$LOCAL_RETRY_ROOT/local-retry"
134
+ printf 'Selected Work Item: local:local-retry\n' > "$LOCAL_RETRY_ROOT/local-retry/local-retry--pull-work.md"
46
135
  printf 'not a run-store directory\n' >"$LOCAL_RETRY_PROJECT/.kontourai/flow"
47
136
  set +e
48
137
  (cd "$LOCAL_RETRY_PROJECT" && CODEX_SESSION_ID=public-workflow-eval npx --yes --package="file:$TARBALL" flow-agents-workflow-sidecar ensure-session \
@@ -50,7 +139,8 @@ set +e
50
139
  --title "Local retry" --summary "Resume the bound local workflow." --flow-id builder.build >/dev/null 2>&1)
51
140
  LOCAL_SEED_RC=$?
52
141
  set -e
53
- [[ "$LOCAL_SEED_RC" -ne 0 ]] || fail "local retry fixture unexpectedly started against an invalid Flow store"
142
+ [[ "$LOCAL_SEED_RC" -eq 0 && ! -e "$LOCAL_RETRY_PROJECT/.kontourai/flow/runs/local-retry/state.json" ]] || fail "private writer advanced canonical Flow instead of only seeding the public retry"
143
+ pass "private workflow writer cannot start or advance canonical Flow"
54
144
  LOCAL_RETRY_COMMAND="$(node -p "JSON.parse(require('fs').readFileSync('$LOCAL_RETRY_ROOT/local-retry/state.json')).next_action.command")"
55
145
  [[ "$LOCAL_RETRY_COMMAND" == *"'--work-item' 'local:local-retry'"* && "$LOCAL_RETRY_COMMAND" == *"'--task-slug' 'local-retry'"* && "$LOCAL_RETRY_COMMAND" == *"'--artifact-root' '$LOCAL_RETRY_ROOT'"* ]] || fail "emitted local retry did not bind its Work Item, slug, and originating artifact root"
56
146
  rm -f "$LOCAL_RETRY_PROJECT/.kontourai/flow"
@@ -61,12 +151,8 @@ EXECUTABLE_RETRY="$(node -e 'process.stdout.write(process.argv[1].replace(proces
61
151
  [[ -f "$LOCAL_RETRY_PROJECT/.kontourai/flow/runs/local-retry/state.json" && ! -e "$FOREIGN_RETRY_CWD/.kontourai" ]] || fail "emitted local retry mutated the caller cwd instead of the originating store"
62
152
  pass "emitted local retry executes from a foreign cwd against its exact originating store"
63
153
 
64
- snapshot_tree() {
65
- local root="$1"
66
- find "$root" -type f -print0 | sort -z | xargs -0 shasum -a 256
67
- }
68
154
  BEFORE_STATUS="$(snapshot_tree "$CONSUMER/.kontourai")"
69
- STATUS_JSON="$(run_candidate status --artifact-root "$ARTIFACT_ROOT" --json)"
155
+ STATUS_JSON="$(run_candidate status --session-dir "$RELEASE_SESSION" --json)"
70
156
  AFTER_STATUS="$(snapshot_tree "$CONSUMER/.kontourai")"
71
157
  [[ "$BEFORE_STATUS" == "$AFTER_STATUS" ]] || fail "workflow status mutated durable artifacts"
72
158
  node -e 'const r=JSON.parse(process.argv[1]); if(r.definition_id!=="builder.build"||r.current_step!=="design-probe")process.exit(1)' "$STATUS_JSON" || fail "status did not report canonical run"
@@ -74,10 +160,187 @@ pass "status is canonical and byte-read-only"
74
160
 
75
161
  FLOW_MANIFEST="$CONSUMER/.kontourai/flow/runs/acme-widgets-101/evidence/manifest.json"
76
162
  BEFORE_EVIDENCE="$(node -p "JSON.parse(require('fs').readFileSync('$FLOW_MANIFEST')).evidence.length")"
77
- run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation pickup-probe-readiness --status not_verified --summary "Consumer fixture intentionally leaves this claim unverified." --json >/dev/null
163
+ PARTIAL_EVIDENCE="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation pickup-probe-readiness --status not_verified --summary "Consumer fixture intentionally leaves this claim unverified." --json)"
78
164
  AFTER_EVIDENCE="$(node -p "JSON.parse(require('fs').readFileSync('$FLOW_MANIFEST')).evidence.length")"
79
- [[ "$AFTER_EVIDENCE" -eq $((BEFORE_EVIDENCE + 1)) ]] || fail "evidence invocation did not attach exactly once"
80
- pass "evidence records and synchronizes exactly once"
165
+ [[ "$AFTER_EVIDENCE" -eq "$BEFORE_EVIDENCE" ]] || fail "partial evidence changed the canonical manifest"
166
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.attached!==false||r.awaiting_evidence!==true||r.current_step!=="design-probe")process.exit(1)' "$PARTIAL_EVIDENCE" || fail "partial evidence did not report an explicit awaiting-evidence result"
167
+ pass "partial evidence records locally without evaluation or canonical attachment"
168
+
169
+ PULL_REPORT="$RELEASE_SESSION/$(basename "$RELEASE_SESSION")--pull-work.md"
170
+ PULL_REPORT_REF="{\"kind\":\"artifact\",\"file\":\"$PULL_REPORT\",\"summary\":\"Concrete selected-work and probe report.\"}"
171
+ READINESS_PARTIAL="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation pickup-probe-readiness --status pass --summary "Complete the consumer readiness expectation after exercising NOT_VERIFIED." --evidence-ref-json "$PULL_REPORT_REF" --json)"
172
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.attached!==false||r.awaiting_evidence!==true||r.current_step!=="design-probe")process.exit(1)' "$READINESS_PARTIAL" || fail "first passing member of a multi-expectation gate did not remain pending"
173
+ PROBE_COMPLETE="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation probe-decisions-or-accepted-gaps --status pass --summary "Complete the consumer probe gate." --evidence-ref-json "$PULL_REPORT_REF" --json)"
174
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.attached!==true||r.awaiting_evidence!==false||r.current_step!=="plan")process.exit(1)' "$PROBE_COMPLETE" || fail "complete multi-expectation gate did not attach and advance exactly once"
175
+ AFTER_COMPLETE_EVIDENCE="$(node -p "JSON.parse(require('fs').readFileSync('$FLOW_MANIFEST')).evidence.length")"
176
+ [[ "$AFTER_COMPLETE_EVIDENCE" -eq $((BEFORE_EVIDENCE + 1)) ]] || fail "complete multi-expectation gate did not attach exactly once"
177
+ pass "multi-expectation evidence attaches atomically only when complete"
178
+ seed_pull_work acme/widgets#104
179
+ run_candidate_as poison-pointer start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#104 --assignment-provider local-file --summary "Poison global pointer fixture" >/dev/null
180
+ node - "$ARTIFACT_ROOT" "$(basename "$RELEASE_SESSION")" <<'NODE'
181
+ const fs = require('node:fs');
182
+ const path = require('node:path');
183
+ const [root, releaseSlug] = process.argv.slice(2);
184
+ const actorRoot = path.join(root, 'current');
185
+ for (const name of fs.readdirSync(actorRoot)) {
186
+ if (!name.endsWith('.json')) continue;
187
+ const file = path.join(actorRoot, name);
188
+ const pointer = JSON.parse(fs.readFileSync(file, 'utf8'));
189
+ if (pointer.active_slug !== releaseSlug) continue;
190
+ pointer.active_slug = 'acme-widgets-104';
191
+ pointer.artifact_dir = 'acme-widgets-104';
192
+ pointer.active_step_id = 'design-probe';
193
+ fs.writeFileSync(file, `${JSON.stringify(pointer, null, 2)}\n`);
194
+ }
195
+ NODE
196
+ PLAN_REPORT="$RELEASE_SESSION/$(basename "$RELEASE_SESSION")--plan-work.md"
197
+ printf '# Plan Work\n\nReviewed fixture plan and acceptance mapping.\n' > "$PLAN_REPORT"
198
+ PLAN_REPORT_REF="{\"kind\":\"artifact\",\"file\":\"$PLAN_REPORT\",\"summary\":\"Reviewed implementation plan.\"}"
199
+ EXACT_SESSION_RESULT="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation implementation-plan --status pass --summary "Exact session binding must ignore the poisoned global pointer." --evidence-ref-json "$PLAN_REPORT_REF" --json)"
200
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.current_step!=="execute"||!String(r.next_action?.summary).includes("`execute`"))process.exit(1)' "$EXACT_SESSION_RESULT" || fail "public evidence followed a poisoned global pointer or returned a stale next action"
201
+ pass "evidence binds the exact session and returns the locked post-transition next action"
202
+ set +e
203
+ UNRELATED_EVIDENCE="$(run_candidate_as unrelated-caller evidence --session-dir "$RELEASE_SESSION" --expectation implementation-scope --status pass --summary rejected 2>&1)"
204
+ UNRELATED_EVIDENCE_RC=$?
205
+ set -e
206
+ [[ "$UNRELATED_EVIDENCE_RC" -ne 0 && "$UNRELATED_EVIDENCE" == *"active, matching assignment actor"* ]] || fail "public evidence allowed a non-holder to impersonate the assignment actor"
207
+ pass "evidence rejects callers that do not match the exact session assignment"
208
+
209
+ STALE_STATE="$TMP/stale-state.json"
210
+ cp "$RELEASE_SESSION/state.json" "$STALE_STATE"
211
+ node - "$RELEASE_SESSION/state.json" <<'NODE'
212
+ const fs = require('node:fs');
213
+ const file = process.argv[2];
214
+ const state = JSON.parse(fs.readFileSync(file, 'utf8'));
215
+ state.flow_run.current_step = 'pull-work';
216
+ state.next_action = { status: 'continue', summary: 'tampered projection' };
217
+ fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
218
+ NODE
219
+ DELIVER_REPORT="$RELEASE_SESSION/$(basename "$RELEASE_SESSION")--deliver.md"
220
+ DELIVER_REPORT_REF="{\"kind\":\"artifact\",\"file\":\"$DELIVER_REPORT\",\"summary\":\"Executed implementation scope report.\"}"
221
+ REPAIRED_EVIDENCE_RESULT="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation implementation-scope --status pass --summary "Repair stale projection before evidence." --evidence-ref-json "$DELIVER_REPORT_REF" --json)"
222
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.current_step!=="verify"||!String(r.next_action?.summary).includes("`verify`"))process.exit(1)' "$REPAIRED_EVIDENCE_RESULT" || fail "evidence did not return its locked post-transition report"
223
+ pass "evidence returns immutable postconditions captured while the subject lock is held"
224
+ FAILED_ROUTE_RESULT="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation tests-evidence --status fail --route-reason implementation_defect --summary "Failing fixture routes back to execution." --command false --json)"
225
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.current_step!=="execute"||!String(r.next_action?.summary).includes("implementation_defect"))process.exit(1)' "$FAILED_ROUTE_RESULT" || fail "failed command evidence did not select the declared canonical route-back"
226
+ REENTER_VERIFY_RESULT="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation implementation-scope --status pass --summary "Corrected implementation re-enters verification." --evidence-ref-json "$DELIVER_REPORT_REF" --json)"
227
+ node -e 'const r=JSON.parse(process.argv[1]);if(r.current_step!=="verify")process.exit(1)' "$REENTER_VERIFY_RESULT" || fail "corrected implementation did not re-enter verification"
228
+ pass "public evidence records failing command observations and declared route-back reasons"
229
+ set +e
230
+ MISSING_TEST_COMMAND="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation tests-evidence --status pass --summary "Missing runnable test command." 2>&1)"
231
+ MISSING_TEST_COMMAND_RC=$?
232
+ FAILED_TEST_COMMAND="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation tests-evidence --status pass --summary "Failing test command." --command 'false' 2>&1)"
233
+ FAILED_TEST_COMMAND_RC=$?
234
+ VERSION_TEST_COMMAND="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation tests-evidence --status pass --summary "Version-only test command." --command 'node --version' 2>&1)"
235
+ VERSION_TEST_COMMAND_RC=$?
236
+ set -e
237
+ [[ "$MISSING_TEST_COMMAND_RC" -ne 0 && "$MISSING_TEST_COMMAND" == *"--command"* && "$FAILED_TEST_COMMAND_RC" -ne 0 && "$FAILED_TEST_COMMAND" == *"non-vacuous package script"* && "$VERSION_TEST_COMMAND_RC" -ne 0 && "$VERSION_TEST_COMMAND" == *"non-vacuous package script"* ]] || fail "tests-evidence did not require a real passing fixture assertion"
238
+ REJECTED_COMMAND_MARKER="$TMP/rejected-command-marker"
239
+ cp "$RELEASE_SESSION/state.json" "$TMP/rejected-command-state.clean.json"
240
+ node - "$RELEASE_SESSION/state.json" <<'NODE'
241
+ const fs = require('node:fs');
242
+ const file = process.argv[2];
243
+ const state = JSON.parse(fs.readFileSync(file, 'utf8'));
244
+ state.flow_run.current_step = 'pull-work';
245
+ state.next_action = { status: 'continue', summary: 'stale projection must remain untouched on rejection' };
246
+ fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
247
+ NODE
248
+ REJECTED_COMMAND_BEFORE="$(snapshot_tree "$RELEASE_SESSION")"
249
+ set +e
250
+ REJECTED_COMMAND="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation tests-evidence --status pass --summary "Rejected prose command must not run." --command "This prose command is invalid; touch '$REJECTED_COMMAND_MARKER'" 2>&1)"
251
+ REJECTED_COMMAND_RC=$?
252
+ set -e
253
+ REJECTED_COMMAND_AFTER="$(snapshot_tree "$RELEASE_SESSION")"
254
+ [[ "$REJECTED_COMMAND_RC" -ne 0 && "$REJECTED_COMMAND" == *"not a runnable shell command"* && ! -e "$REJECTED_COMMAND_MARKER" && "$REJECTED_COMMAND_BEFORE" == "$REJECTED_COMMAND_AFTER" ]] || fail "rejected evidence command executed or mutated durable artifacts"
255
+ mv "$TMP/rejected-command-state.clean.json" "$RELEASE_SESSION/state.json"
256
+ pass "rejected evidence commands have no process or durable-artifact side effects"
257
+ BEFORE_CRITIQUE_MANIFEST="$(shasum -a 256 "$FLOW_MANIFEST" | awk '{print $1}')"
258
+ BEFORE_CRITIQUE_BUNDLE="$(shasum -a 256 "$RELEASE_SESSION/trust.bundle" | awk '{print $1}')"
259
+ CODE_LANE="{\"id\":\"code\",\"status\":\"pass\",\"summary\":\"Code review covered the planned implementation.\",\"evidence_refs\":[{\"kind\":\"artifact\",\"file\":\"$PLAN_REPORT\",\"summary\":\"Reviewed implementation plan.\"}]}"
260
+ SECURITY_LANE="{\"id\":\"security\",\"status\":\"pass\",\"summary\":\"Security review covered the delivered scope.\",\"evidence_refs\":[{\"kind\":\"artifact\",\"file\":\"$DELIVER_REPORT\",\"summary\":\"Reviewed delivered scope.\"}]}"
261
+ set +e
262
+ SAME_ACTOR_CRITIQUE="$(run_candidate critique --session-dir "$RELEASE_SESSION" --id public-review --verdict pass --summary "Implementation actor cannot review its own work." --artifact-ref "$DELIVER_REPORT" --lane-json "$CODE_LANE" --lane-json "$SECURITY_LANE" 2>&1)"
263
+ SAME_ACTOR_CRITIQUE_RC=$?
264
+ set -e
265
+ [[ "$SAME_ACTOR_CRITIQUE_RC" -ne 0 && "$SAME_ACTOR_CRITIQUE" == *"reviewer identity distinct"* ]] || fail "public critique allowed the active implementation actor to self-review"
266
+ run_candidate_as public-reviewer critique --session-dir "$RELEASE_SESSION" --id public-review --verdict pass --summary "Public critique fixture." --artifact-ref "$DELIVER_REPORT" --lane-json "$CODE_LANE" --lane-json "$SECURITY_LANE" --json >/dev/null
267
+ AFTER_CRITIQUE_MANIFEST="$(shasum -a 256 "$FLOW_MANIFEST" | awk '{print $1}')"
268
+ AFTER_CRITIQUE_BUNDLE="$(shasum -a 256 "$RELEASE_SESSION/trust.bundle" | awk '{print $1}')"
269
+ [[ "$BEFORE_CRITIQUE_MANIFEST" == "$AFTER_CRITIQUE_MANIFEST" && "$BEFORE_CRITIQUE_BUNDLE" != "$AFTER_CRITIQUE_BUNDLE" ]] || fail "public critique attached to Flow or did not change trust.bundle"
270
+ pass "critique requires an active assignment but rejects self-review by its implementation actor"
271
+ CRITERION_ID="$(node -p "JSON.parse(require('fs').readFileSync('$RELEASE_SESSION/acceptance.json')).criteria[0].id")"
272
+ TEST_COMMAND="bash checks/check-public-workflow.sh .kontourai/flow-agents/$(basename "$RELEASE_SESSION")/state.json"
273
+ MULTI_COMMAND_ONE="$TMP/multi-command-one"
274
+ MULTI_COMMAND_TWO="$TMP/multi-command-two"
275
+ TEST_COMMAND_TWO="bash checks/check-multi-command.sh .kontourai/flow-agents/$(basename "$RELEASE_SESSION")/state.json '$MULTI_COMMAND_ONE'"
276
+ TEST_COMMAND_THREE="bash checks/check-multi-command.sh .kontourai/flow-agents/$(basename "$RELEASE_SESSION")/state.json '$MULTI_COMMAND_TWO'"
277
+ CRITERION_JSON="$(node - "$CRITERION_ID" "$TEST_COMMAND" "$TEST_COMMAND_TWO" "$TEST_COMMAND_THREE" <<'NODE'
278
+ const [id, ...commands] = process.argv.slice(2);
279
+ process.stdout.write(JSON.stringify({ id, status: 'pass', evidence_refs: commands.map((excerpt) => ({ kind: 'command', excerpt, summary: 'Asserts the bound session state exists.' })) }));
280
+ NODE
281
+ )"
282
+ COMMAND_REF="$(node - "$TEST_COMMAND" <<'NODE'
283
+ const command = process.argv[2];
284
+ process.stdout.write(JSON.stringify({ kind: 'command', excerpt: command, summary: 'Exact project-local public workflow check.' }));
285
+ NODE
286
+ )"
287
+ COMMAND_REF_TWO="$(node - "$TEST_COMMAND_TWO" <<'NODE'
288
+ const command = process.argv[2];
289
+ process.stdout.write(JSON.stringify({ kind: 'command', excerpt: command, summary: 'Exact first additional project-local check.' }));
290
+ NODE
291
+ )"
292
+ COMMAND_REF_THREE="$(node - "$TEST_COMMAND_THREE" <<'NODE'
293
+ const command = process.argv[2];
294
+ process.stdout.write(JSON.stringify({ kind: 'command', excerpt: command, summary: 'Exact second additional project-local check.' }));
295
+ NODE
296
+ )"
297
+ run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation tests-evidence --status pass --summary "Passing fixture assertion." --command "$TEST_COMMAND" --command "$TEST_COMMAND_TWO" --command "$TEST_COMMAND_THREE" --evidence-ref-json "$COMMAND_REF" --evidence-ref-json "$COMMAND_REF_TWO" --evidence-ref-json "$COMMAND_REF_THREE" --criterion-json "$CRITERION_JSON" --json >/dev/null
298
+ node - "$RELEASE_SESSION/trust.bundle" <<'NODE'
299
+ const fs = require('node:fs');
300
+ const bundle = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
301
+ const claim = bundle.claims.find((entry) => entry.metadata?.gate_claim?.expectation_id === 'tests-evidence');
302
+ if (!claim || claim.metadata?.output_digest?.algorithm !== 'sha256' || typeof claim.metadata.output_digest.hex !== 'string' || claim.metadata?.observed_commands?.length !== 3) process.exit(1);
303
+ NODE
304
+ [[ "$(wc -l < "$MULTI_COMMAND_ONE" | tr -d ' ')" == 1 && "$(wc -l < "$MULTI_COMMAND_TWO" | tr -d ' ')" == 1 ]] || fail "public evidence did not execute every repeated --command exactly once"
305
+ pass "tests-evidence executes every repeated command once and records matching observations"
306
+
307
+ ASSIGNMENT="$ARTIFACT_ROOT/assignment/$(basename "$RELEASE_SESSION").json"
308
+ ASSIGNMENT_TARGET="$TMP/assignment-target.json"
309
+ cp "$ASSIGNMENT" "$ASSIGNMENT_TARGET"
310
+ rm "$ASSIGNMENT"
311
+ ln -s "$ASSIGNMENT_TARGET" "$ASSIGNMENT"
312
+ set +e
313
+ SYMLINK_ASSIGNMENT="$(run_candidate critique --session-dir "$RELEASE_SESSION" --summary "Rejected assignment symlink." 2>&1)"
314
+ SYMLINK_ASSIGNMENT_RC=$?
315
+ set -e
316
+ rm "$ASSIGNMENT"
317
+ mv "$ASSIGNMENT_TARGET" "$ASSIGNMENT"
318
+ [[ "$SYMLINK_ASSIGNMENT_RC" -ne 0 && "$SYMLINK_ASSIGNMENT" == *"assignment must be a non-symlink regular file"* ]] || fail "public critique followed a symlinked assignment"
319
+ pass "public assignment readers reject symlinked assignment files"
320
+ node - "$RELEASE_SESSION/trust.bundle" "$RELEASE_SESSION" <<'NODE'
321
+ const fs = require('node:fs');
322
+ const [bundleFile, session] = process.argv.slice(2);
323
+ const bundle = JSON.parse(fs.readFileSync(bundleFile, 'utf8'));
324
+ const critique = bundle.claims.find((claim) => claim.metadata?.origin === 'critique');
325
+ if (!critique || critique.claimType !== 'workflow.critique.review' || critique.metadata?.lanes?.length !== 2 || fs.existsSync(`${session}/critique.json`) || fs.existsSync(`${session}/evidence.json`)) process.exit(1);
326
+ NODE
327
+ pass "public critique is report-only, forwards lanes, and writes only to trust.bundle"
328
+
329
+ cp "$RELEASE_SESSION/state.json" "$TMP/release-state.clean.json"
330
+ node - "$RELEASE_SESSION/state.json" <<'NODE'
331
+ const fs = require('node:fs');
332
+ const file = process.argv[2];
333
+ const state = JSON.parse(fs.readFileSync(file, 'utf8'));
334
+ state.task_slug = '../outside';
335
+ fs.writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`);
336
+ NODE
337
+ set +e
338
+ TAMPERED_SLUG="$(run_candidate evidence --session-dir "$RELEASE_SESSION" --expectation policy-compliance --status not_verified --summary rejected 2>&1)"
339
+ TAMPERED_SLUG_RC=$?
340
+ set -e
341
+ mv "$TMP/release-state.clean.json" "$RELEASE_SESSION/state.json"
342
+ [[ "$TAMPERED_SLUG_RC" -ne 0 && "$TAMPERED_SLUG" == *"task_slug must exactly match"* ]] || fail "public evidence accepted a tampered task slug"
343
+ pass "public evidence rejects a mismatched or traversal task slug before assignment lookup"
81
344
 
82
345
  OUTSIDE="$TMP/outside-session"
83
346
  mkdir -p "$OUTSIDE"
@@ -97,7 +360,8 @@ run_candidate release --session-dir "$RELEASE_SESSION" --reason "consumer releas
97
360
  node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(process.argv[1]));if(r.status!=="released")process.exit(1)' "$ARTIFACT_ROOT/assignment/acme-widgets-101.json" || fail "release did not release assignment"
98
361
  pass "pause, resume, and release use the public command"
99
362
 
100
- run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#102 --summary "Cancel fixture" >/dev/null
363
+ seed_pull_work acme/widgets#102
364
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#102 --assignment-provider local-file --summary "Cancel fixture" >/dev/null
101
365
  CANCEL_SESSION="$ARTIFACT_ROOT/acme-widgets-102"
102
366
  node --input-type=module - "$CONSUMER" "$CANCEL_SESSION" <<'NODE'
103
367
  import fs from 'node:fs';
@@ -129,6 +393,62 @@ run_candidate archive --session-dir "$CANCEL_SESSION" --authorization-file "$CON
129
393
  [[ -f "$ARTIFACT_ROOT/archive/acme-widgets-102/state.json" && ! -e "$CANCEL_SESSION" ]] || fail "cancel/archive did not retain archived session"
130
394
  pass "signed cancel and archive execute through the public command"
131
395
 
396
+ seed_pull_work acme/widgets#106
397
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#106 --assignment-provider local-file --summary "Command authority lock fixture" >/dev/null
398
+ LOCK_SESSION="$ARTIFACT_ROOT/acme-widgets-106"
399
+ LOCK_PULL_REPORT="$LOCK_SESSION/$(basename "$LOCK_SESSION")--pull-work.md"
400
+ LOCK_PULL_REF="{\"kind\":\"artifact\",\"file\":\"$LOCK_PULL_REPORT\",\"summary\":\"Lock fixture selected work report.\"}"
401
+ run_candidate evidence --session-dir "$LOCK_SESSION" --expectation pickup-probe-readiness --status pass --summary "Complete lock fixture probe." --evidence-ref-json "$LOCK_PULL_REF" --json >/dev/null
402
+ run_candidate evidence --session-dir "$LOCK_SESSION" --expectation probe-decisions-or-accepted-gaps --status pass --summary "Complete lock fixture decisions." --evidence-ref-json "$LOCK_PULL_REF" --json >/dev/null
403
+ LOCK_PLAN_REPORT="$LOCK_SESSION/$(basename "$LOCK_SESSION")--plan-work.md"
404
+ printf '# Lock Plan\n' > "$LOCK_PLAN_REPORT"
405
+ LOCK_PLAN_REF="{\"kind\":\"artifact\",\"file\":\"$LOCK_PLAN_REPORT\",\"summary\":\"Lock fixture plan.\"}"
406
+ run_candidate evidence --session-dir "$LOCK_SESSION" --expectation implementation-plan --status pass --summary "Complete lock fixture plan." --evidence-ref-json "$LOCK_PLAN_REF" --json >/dev/null
407
+ LOCK_DELIVER_REPORT="$LOCK_SESSION/$(basename "$LOCK_SESSION")--deliver.md"
408
+ LOCK_DELIVER_REF="{\"kind\":\"artifact\",\"file\":\"$LOCK_DELIVER_REPORT\",\"summary\":\"Lock fixture execution report.\"}"
409
+ LOCK_STARTED="$TMP/command-lock.started"
410
+ LOCK_COMMAND="bash checks/check-command-lock.sh '$LOCK_STARTED'"
411
+ run_candidate evidence --session-dir "$LOCK_SESSION" --expectation implementation-scope --status pass --summary "Long command retains authority lock." --evidence-ref-json "$LOCK_DELIVER_REF" --command "$LOCK_COMMAND" --json >"$TMP/command-lock-evidence.out" 2>&1 &
412
+ LOCK_EVIDENCE_PID=$!
413
+ for _ in $(seq 1 250); do [[ -f "$LOCK_STARTED" ]] && break; sleep 0.02; done
414
+ [[ -f "$LOCK_STARTED" ]] || fail "authority-lock fixture command did not start"
415
+ run_candidate release --session-dir "$LOCK_SESSION" --reason "release must wait for the observed command" >"$TMP/command-lock-release.out" 2>&1 &
416
+ LOCK_RELEASE_PID=$!
417
+ sleep 0.1
418
+ kill -0 "$LOCK_RELEASE_PID" 2>/dev/null || fail "lifecycle release bypassed the running command lock"
419
+ node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(process.argv[1]));if(r.status!=="claimed")process.exit(1)' "$ARTIFACT_ROOT/assignment/acme-widgets-106.json" || fail "assignment changed before the locked command completed"
420
+ wait "$LOCK_EVIDENCE_PID" || fail "locked evidence command failed"
421
+ wait "$LOCK_RELEASE_PID" || fail "blocked release failed after command completion"
422
+ node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(process.argv[1]));if(r.status!=="released")process.exit(1)' "$ARTIFACT_ROOT/assignment/acme-widgets-106.json" || fail "release did not complete after the command lock was released"
423
+ pass "authority-bound commands retain the subject lock through lifecycle release"
424
+
425
+ seed_pull_work acme/widgets#107
426
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#107 --assignment-provider local-file --summary "Process group timeout fixture" >/dev/null
427
+ TIMEOUT_SESSION="$ARTIFACT_ROOT/acme-widgets-107"
428
+ TIMEOUT_CHILD_PID="$TMP/command-timeout-child.pid"
429
+ TIMEOUT_MARKER="$TMP/command-timeout-marker"
430
+ set +e
431
+ (cd "$CONSUMER" && CODEX_SESSION_ID=public-workflow-eval FLOW_AGENTS_EVIDENCE_COMMAND_TIMEOUT_MS=1000 FLOW_AGENTS_EVIDENCE_COMMAND_KILL_GRACE_MS=250 npx --yes --package="file:$TARBALL" flow-agents workflow evidence --session-dir "$TIMEOUT_SESSION" --expectation pickup-probe-readiness --status not_verified --summary "Timeout fixture captures full process-group termination." --command "bash checks/check-command-timeout.sh '$TIMEOUT_CHILD_PID' '$TIMEOUT_MARKER'" --json) >"$TMP/command-timeout.out" 2>&1
432
+ TIMEOUT_RC=$?
433
+ set -e
434
+ [[ "$TIMEOUT_RC" -ne 0 && -s "$TIMEOUT_CHILD_PID" && ! -e "$TIMEOUT_MARKER" ]] || fail "timed-out evidence command did not complete its controlled capture"
435
+ TIMEOUT_CHILD="$(cat "$TIMEOUT_CHILD_PID")"
436
+ kill -0 "$TIMEOUT_CHILD" 2>/dev/null && fail "timed-out evidence command left a child process running"
437
+ pass "timed-out evidence commands terminate their complete process group"
438
+
439
+ BACKGROUND_CHILD_PID="$TMP/success-background-child.pid"
440
+ BACKGROUND_MARKER="$TMP/success-background-marker"
441
+ TIMEOUT_PULL_REPORT="$TIMEOUT_SESSION/$(basename "$TIMEOUT_SESSION")--pull-work.md"
442
+ (cd "$CONSUMER" && CODEX_SESSION_ID=public-workflow-eval FLOW_AGENTS_EVIDENCE_COMMAND_KILL_GRACE_MS=50 npx --yes --package="file:$TARBALL" flow-agents workflow evidence \
443
+ --session-dir "$TIMEOUT_SESSION" --expectation pickup-probe-readiness --status pass \
444
+ --summary "Successful evidence cleans up surviving background processes." \
445
+ --command "bash checks/check-success-background.sh '$BACKGROUND_CHILD_PID' '$BACKGROUND_MARKER'" \
446
+ --evidence-ref-json "{\"kind\":\"artifact\",\"file\":\"$TIMEOUT_PULL_REPORT\",\"summary\":\"Selected work for background cleanup fixture.\"}" --json >/dev/null)
447
+ [[ -s "$BACKGROUND_CHILD_PID" && ! -e "$BACKGROUND_MARKER" ]] || fail "successful evidence command did not expose a surviving background child"
448
+ BACKGROUND_CHILD="$(cat "$BACKGROUND_CHILD_PID")"
449
+ kill -0 "$BACKGROUND_CHILD" 2>/dev/null && fail "successful evidence command left a background process running"
450
+ pass "successful evidence commands terminate surviving process-group children before recording"
451
+
132
452
  DOCTOR="$TMP/doctor-consumer"
133
453
  mkdir -p "$DOCTOR/node_modules/@kontourai/flow-agents" "$DOCTOR/node_modules/.bin" "$DOCTOR/.flow-agents" "$DOCTOR/kits/builder/flows" "$DOCTOR/.kontourai/flow-agents/doctor-session"
134
454
  cat >"$DOCTOR/node_modules/@kontourai/flow-agents/package.json" <<'JSON'
@@ -210,7 +530,7 @@ MISSING_JSON="$(cd "$DOCTOR" && npx --yes --package="file:$TARBALL" flow-agents
210
530
  MISSING_RC=$?
211
531
  set -e
212
532
  [[ "$MISSING_RC" -eq 2 ]] || fail "doctor should fail when an activated Kit is missing"
213
- node -e 'const r=JSON.parse(process.argv[1]);if(!r.warnings.some(w=>w.includes("Activated Builder Kit is missing"))||!r.warnings.some(w=>w.includes("Activated builder.build definition is missing")))process.exit(1)' "$MISSING_JSON" || fail "doctor did not report missing activated Kit components"
533
+ node -e 'const r=JSON.parse(process.argv[1]);if(!r.warnings.some(w=>w.includes("Activated Builder Kit is missing"))||!r.warnings.some(w=>w.includes("Activated builder.build definition is missing"))||!r.warnings.some(w=>w.includes("Activated builder.shape definition is missing")))process.exit(1)' "$MISSING_JSON" || fail "doctor did not report both missing activated Builder definitions"
214
534
  pass "doctor fails closed for missing activated Kit components"
215
535
 
216
536
  HEALTHY="$TMP/healthy-install"
@@ -126,7 +126,7 @@ fi
126
126
  # duplicate validation logic was introduced for resolve-slug.
127
127
  if flow_agents_node "$WRITER" resolve-slug 'owner/repo' >"$TMPDIR_EVAL/a6.out" 2>"$TMPDIR_EVAL/a6.err"; then
128
128
  _fail "resolve-slug should reject a ref with no # separator"
129
- elif rg -q -- 'owner/repo#id format' "$TMPDIR_EVAL/a6.err"; then
129
+ elif rg -q -- 'provider-neutral provider:id ref or owner/repo#numeric-id' "$TMPDIR_EVAL/a6.err"; then
130
130
  _pass "resolve-slug rejects a ref with no # separator using workItemSlug()'s existing message (AC7)"
131
131
  else
132
132
  _fail "resolve-slug malformed-ref rejection message mismatch: $(cat "$TMPDIR_EVAL/a6.out" "$TMPDIR_EVAL/a6.err")"
@@ -323,71 +323,27 @@ else
323
323
  _fail "liveness status unexpectedly did not acquire the lock — F1's bypass may have leaked beyond the whoami action: out=$(cat "$TMPDIR_EVAL/e3.out") err=$(cat "$TMPDIR_EVAL/e3.err")"
324
324
  fi
325
325
 
326
- # ─── D. Static skill-text assertions (AC6, AC8) ─────────────────────────────
327
- echo "--- D. Static skill-text assertions ---"
328
-
329
- require_text "$PULL" '### 1a\. Liveness Selection Preflight' "pull-work documents the Liveness Selection Preflight subsection (AC1, AC8)"
330
- require_text "$PULL" 'liveness whoami --json' "pull-work references liveness whoami --json by exact command name (AC1, AC8)"
331
- require_text "$PULL" 'liveness status --json' "pull-work references liveness status --json by exact command name (AC1, AC8)"
332
- require_text "$PULL" 'resolve-slug <owner>/<repo>#<issue-number>' "pull-work references resolve-slug by exact command name (AC1, AC4, AC8)"
333
- require_text "$PULL" 'liveness claim <subjectId>' "pull-work references liveness claim <subjectId> by exact command name (AC4, AC8)"
334
- require_text "$PULL" 'reading each row.s raw .status. field \(never .label.\)' "pull-work instructs consuming raw status, never label (AC1, AC8)"
335
- require_text "$PULL" 'a .verified. row for an actor other than self.*.held.' "pull-work documents held classification (AC1, AC2, AC8)"
336
- require_text "$PULL" 'excluded from the ready set by default' "pull-work excludes held candidates by default (AC2, AC8)"
337
- require_text "$PULL" 'effective_state: .reclaimable.' "pull-work documents reclaimable classification via the assignment-provider join (AC1, AC3, AC8, #290)"
338
- require_text "$PULL" 'requires an explicit recorded opt-in' "pull-work requires an explicit recorded opt-in for reclaimable selection (AC3, AC8)"
339
- require_text "$PULL" 'never a silent normal pick' "pull-work states reclaimable is never a silent normal pick (AC3, AC8)"
340
- require_text "$PULL" 'a .verified. row for the resolved self actor.*.mine.' "pull-work documents mine classification (AC1, AC5, AC8)"
341
- require_text "$PULL" 'do not re-offer as new, do not exclude as held-by-other' "pull-work folds own-actor mine into WIP logic, not re-offered or re-excluded (AC5, AC8)"
342
- require_text "$PULL" 'An explicit user instruction to proceed despite a .held. or .reclaimable. classification' "pull-work documents the --force/explicit-instruction override (AC2, AC3, AC8)"
343
- require_text "$PULL" '.--force.' "pull-work names --force as the override mechanism (AC2, AC8)"
344
- require_text "$PULL" 'override and its stated reason must be recorded in the artifact' "pull-work requires the override + reason to be recorded (AC2, AC8)"
345
- require_text "$PULL" 'do not mutate GitHub Projects, provider fields, or cross-repo provider state from this skill' "pull-work's existing no-provider-mutation sentence is present verbatim and unchanged (AC6, AC8)"
346
- require_text "$PULL" 'read/write the local runtime liveness stream, never GitHub issue/label/assignee state' "pull-work states the liveness claim is local runtime state, not a provider mutation (AC6, AC8)"
347
- require_text "$PULL" 'this is not\s*$|this is not a provider mutation' "pull-work explicitly states the liveness claim is not a provider mutation (AC6, AC8)"
348
- require_text "$PULL" '#290' "pull-work names #290 as the deferred assignment-provider seam (AC6, AC8)"
349
- require_text "$PULL" 'liveness_preflight' "pull-work artifact contract records liveness_preflight (AC1, AC8)"
350
- require_text "$PULL" 'liveness_claim' "pull-work artifact contract records liveness_claim (AC4, AC8)"
351
- require_text "$PICKUP_PROBE" 'flags the selected item .reclaimable., treat the recorded opt-in as a decision to re-confirm' "pickup-probe re-confirms (not silently accepts) a reclaimable opt-in on drift (AC3, AC8)"
352
-
353
- # ─── F2-F6 (fix-plan iteration 1) static assertions ─────────────────────────
354
- require_text "$PULL" 'group all rows for that .subjectId. by subject' "pull-work classifies per-subject (grouped rows), not per-row (F2, AC1, AC8)"
355
- require_text "$PULL" 'classify in this precedence order' "pull-work states the classification is an ordered precedence rule (F2, AC8)"
356
- # F2's original numbered precedence list was liveness-only (#166); #290 (Wave 3) replaced it with
357
- # the full ADR 0021 §1 assignment ⋈ liveness join, computed via `assignment-provider status`'s
358
- # `effective_state`/`reason` fields. These assertions are updated to match the new five-row list
359
- # (mine/held/reclaimable/human-held/free) rather than the old four-row liveness-only list.
360
- require_text "$PULL" '^1\. .effective_state: .held.. with .reason: .self_is_holder..' "pull-work's precedence rule step 1 (mine, via self_is_holder) is numbered verbatim (F2, #290, AC8)"
361
- require_text "$PULL" '^2\. else .effective_state: .held..' "pull-work's precedence rule step 2 (held, excluded) is numbered verbatim (F2, #290, AC8)"
362
- require_text "$PULL" '^3\. else .effective_state: .reclaimable..' "pull-work's precedence rule step 3 (reclaimable) is numbered verbatim (F2, #290, AC8)"
363
- require_text "$PULL" '^4\. else .effective_state: .human-held..' "pull-work's precedence rule step 4 (human-held) is numbered verbatim (F2, #290, AC11, AC8)"
364
- require_text "$PULL" '^5\. else .effective_state: .free..' "pull-work's precedence rule step 5 (free) is numbered verbatim (F2, #290, AC8)"
365
- require_text "$PULL" 'double-hold' "pull-work names the own-actor + other-actor co-existing row combination a double-hold (F2, AC8)"
366
- require_text "$PULL" 'explicit conflict warning \(per ADR 0012 §4 detection' "pull-work cites ADR 0012 §4 detection for the double-hold conflict warning (F2, AC8)"
367
- require_text "$PULL" 'never silently resolve it to .mine.' "pull-work states a double-hold is never silently resolved to mine (F2, AC8)"
368
-
369
- require_text "$PULL" 'pin it for reuse' "pull-work pins self_actor for reuse across the pass (F3, AC8)"
370
- require_text "$PULL" 'Capture the returned .actor. value as .self_actor.' "pull-work captures self_actor exactly once per pass from whoami (F3, AC8)"
371
- require_text "$PULL" 'liveness claim <subjectId> --actor <self_actor>' "pull-work's claim-on-selection command passes --actor <self_actor> explicitly (F3, AC4, AC8)"
372
- require_text "$PULL" 'always pass .--actor <self_actor>. explicitly on every claim in this pass' "pull-work requires --actor <self_actor> on every claim in the pass, not just the first (F3, AC8)"
373
- require_text "$PULL" 'never a fresh derivation per claim call' "pull-work states the actor is never re-derived per claim call (F3, AC8)"
374
-
375
- require_text "$PULL" 'On any claim-emit failure \(non-zero exit, unresolved actor, or liveness disabled/unavailable\)' "pull-work names the precise claim-emit failure modes covered by fail-open (F4, AC8)"
376
- require_text "$PULL" '\{skipped: <stderr reason>\}' "pull-work's fail-open shape records {skipped: <stderr reason>} (F4, AC8)"
377
- require_text "$PULL" 'also surface that skip reason in pull-work.s user-facing output' "pull-work requires the skip reason to be surfaced in user-facing output, never silent (F4, AC8)"
378
- require_text "$PULL" 'unresolved-actor failure.? additionally names? the remediation' "pull-work requires unresolved-actor failures to additionally name the remediation (F4, AC8)"
379
-
380
- require_text "$PULL" '.reclaimable_override.: recorded only when a .reclaimable.' "pull-work's Artifact Contract declares the reclaimable_override field (F5, AC8)"
381
-
382
- require_text "$PULL" '### Post-Claim Conflict Re-check' "pull-work documents the Post-Claim Conflict Re-check subsection (F6, AC8)"
383
- require_text "$PULL" 'coexists with this session.s own just-emitted claim on the same subject' "pull-work's post-claim re-check detects a double-hold against the just-emitted claim (F6, AC8)"
384
- require_text "$PULL" 'surface the conflict prominently in the user-facing output and instruct the user to coordinate before proceeding' "pull-work instructs surfacing the post-claim conflict prominently and coordinating before proceeding, not silently continuing (F6, AC8)"
385
- require_text "$PULL" 'does not provide true mutual exclusion across the read-then-write race window itself' "pull-work's honesty note states the exclusion guarantee still does not provide true mutual exclusion across the read-then-write race window (F6, AC8; updated #320 wording — see also test_liveness_verdict.sh)"
386
- require_text "$PULL" '.#290.\x27s provider assignment lease closes this gap for the .{0,2}local-file.{0,2} provider only' "pull-work scopes true mutual exclusion to the local-file provider only, via #290's lock (fix-plan iteration 1, F5, AC8)"
387
- require_text "$PULL" 'two concurrent local-file .claim. calls on the same subject genuinely cannot both win' "pull-work states local-file claim/claim races genuinely cannot both win under #290's lock (fix-plan iteration 1, F5, AC8)"
388
- require_text "$PULL" 'The .{0,2}GitHub.{0,2} provider \(assignee/label/claim-comment\) remains advisory/last-writer' "pull-work does NOT overclaim true mutual exclusion for the GitHub provider — advisory/last-writer, detect not prevent (fix-plan iteration 1, F5, AC8)"
389
- require_text "$PULL" 'Do not read .provider assignment lease. as closing the GitHub race; only the .verify-hold. publish gate \(#293\)' "pull-work explicitly warns not to read #290 as closing the GitHub race — only #293's verify-hold gate would (fix-plan iteration 1, F5, AC8)"
390
-
326
+ # ─── D. Provider-neutral skill contract assertions ─────────────────────────
327
+ echo "--- D. Provider-neutral skill contract assertions ---"
328
+
329
+ require_text "$PULL" 'Assignment And Liveness Selection Preflight' "pull-work documents assignment/liveness preflight"
330
+ require_text "$PULL" 'AssignmentProvider\.status' "pull-work consumes assignment status through the provider"
331
+ require_text "$PULL" 'classify the subject, not individual observation rows' "pull-work classifies grouped subject state"
332
+ require_text "$PULL" 'in this precedence' "pull-work documents ordered ownership states"
333
+ require_text "$PULL" 'raw provider status' "pull-work consumes raw status instead of display labels"
334
+ require_text "$PULL" 'double-hold conflict' "pull-work detects own/other live conflicts"
335
+ require_text "$PULL" 'Exclude it by default' "pull-work excludes held work by default"
336
+ require_text "$PULL" 'explicit recorded opt-in' "pull-work requires explicit reclaimable takeover"
337
+ require_text "$PULL" 'reclaimable_override' "pull-work records reclaimable overrides"
338
+ require_text "$PULL" 'takeover is resumption, not restart' "pull-work preserves incumbent continuation"
339
+ require_text "$PULL" 'AssignmentProvider\.claim' "pull-work claims through the provider boundary"
340
+ require_text "$PULL" 'After .AssignmentProvider\.claim' "pull-work rechecks ownership after claim"
341
+ require_text "$PULL" 'post-claim' "pull-work records post-claim conflicts"
342
+ require_text "$PULL" 'does not imply universal mutual exclusion' "pull-work does not overclaim race prevention"
343
+ require_text "$PULL" 'local-file provider' "pull-work scopes serialized claims to capable providers"
344
+ require_text "$PULL" 'without compare-and-swap' "pull-work treats non-atomic remote providers honestly"
345
+ require_text "$PULL" 'unrelated provider state' "pull-work limits provider mutation"
346
+ require_text "$PICKUP_PROBE" 're-confirm the recorded takeover opt-in' "pickup-probe re-confirms reclaimable takeover after drift"
391
347
 
392
348
  echo ""
393
349
  if [[ "$errors" -eq 0 ]]; then