@kontourai/flow-agents 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.github/workflows/ci.yml +4 -0
  2. package/CHANGELOG.md +8 -0
  3. package/build/src/builder-flow-run-adapter.d.ts +10 -1
  4. package/build/src/builder-flow-run-adapter.js +29 -1
  5. package/build/src/builder-flow-runtime.d.ts +18 -0
  6. package/build/src/builder-flow-runtime.js +205 -13
  7. package/build/src/builder-lifecycle-authority.d.ts +35 -0
  8. package/build/src/builder-lifecycle-authority.js +219 -0
  9. package/build/src/cli/assignment-provider.d.ts +10 -0
  10. package/build/src/cli/assignment-provider.js +61 -52
  11. package/build/src/cli/builder-run.js +46 -5
  12. package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
  13. package/build/src/cli/workflow-sidecar.d.ts +3 -0
  14. package/build/src/cli/workflow-sidecar.js +28 -6
  15. package/build/src/cli/workflow.d.ts +2 -0
  16. package/build/src/cli/workflow.js +521 -0
  17. package/build/src/cli.js +2 -0
  18. package/build/src/index.d.ts +4 -0
  19. package/build/src/index.js +2 -0
  20. package/build/src/lib/package-version.d.ts +2 -0
  21. package/build/src/lib/package-version.js +13 -0
  22. package/build/src/lib/pinned-cli-command.d.ts +6 -0
  23. package/build/src/lib/pinned-cli-command.js +21 -0
  24. package/context/contracts/artifact-contract.md +1 -1
  25. package/context/scripts/hooks/config-protection.js +8 -1
  26. package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
  27. package/context/scripts/hooks/stop-goal-fit.js +1 -1
  28. package/docs/context-map.md +2 -0
  29. package/docs/public-workflow-cli.md +63 -0
  30. package/docs/spec/builder-flow-runtime.md +37 -0
  31. package/docs/workflow-usage-guide.md +5 -0
  32. package/evals/ci/run-baseline.sh +2 -0
  33. package/evals/integration/test_builder_entry_enforcement.sh +5 -4
  34. package/evals/integration/test_bundle_install.sh +59 -5
  35. package/evals/integration/test_public_workflow_cli.sh +259 -0
  36. package/package.json +2 -2
  37. package/schemas/builder-lifecycle-authorization.schema.json +57 -0
  38. package/schemas/lifecycle-authority-keys.schema.json +25 -0
  39. package/schemas/workflow-state.schema.json +1 -1
  40. package/scripts/hooks/config-protection.js +8 -1
  41. package/scripts/hooks/lib/config-protection-remedies.js +3 -0
  42. package/scripts/hooks/stop-goal-fit.js +1 -1
  43. package/src/builder-flow-run-adapter.ts +47 -0
  44. package/src/builder-flow-runtime.ts +216 -4
  45. package/src/builder-lifecycle-authority.ts +218 -0
  46. package/src/cli/assignment-provider.ts +29 -9
  47. package/src/cli/builder-flow-runtime.test.mjs +404 -1
  48. package/src/cli/builder-run.ts +56 -5
  49. package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
  50. package/src/cli/workflow-sidecar.ts +28 -6
  51. package/src/cli/workflow.ts +471 -0
  52. package/src/cli.ts +2 -0
  53. package/src/index.ts +14 -0
  54. package/src/lib/package-version.ts +15 -0
  55. package/src/lib/pinned-cli-command.ts +23 -0
@@ -20,6 +20,8 @@ const SANCTIONED_REMEDIES = {
20
20
  'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
21
21
  '.claude/settings.local.json':
22
22
  'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
23
+ '.flow-agents/lifecycle-authority-keys.json':
24
+ 'Only the trusted harness installer or a human maintainer may rotate lifecycle authority keys. Never let an agent replace this trust root.',
23
25
  '.kontourai/flow-agents/current.json':
24
26
  'Use `npm run workflow:sidecar -- ensure-session` (or `advance-state`), which writes this file for you. Never disable this hook to make the write.',
25
27
  '.kontourai/flow-agents/current/<actor>.json':
@@ -58,6 +60,7 @@ function remedyFor(name) {
58
60
  * basename 'trust.bundle') -- first match wins, deterministically.
59
61
  */
60
62
  const REMEDY_COMMAND_CANDIDATES = [
63
+ { name: '.flow-agents/lifecycle-authority-keys.json', needles: ['lifecycle-authority-keys.json'] },
61
64
  { name: 'delivery/trust.checkpoint.json', needles: ['delivery/trust.checkpoint.json', 'trust.checkpoint.json'] },
62
65
  { name: 'delivery/trust.bundle', needles: ['delivery/trust.bundle'] },
63
66
  { name: '.kontourai/flow-agents/<slug>/trust.bundle', needles: ['trust.bundle'] },
@@ -77,7 +77,7 @@ const PRE_EXECUTION_PHASES = new Set(['idea', 'backlog', 'pickup', 'planning']);
77
77
  // Terminal tasks are complete — they must never gate a stop or count as "active".
78
78
  // A stale current.json pointing at one, or a graveyard of finished states, must
79
79
  // not block an unrelated session.
80
- const TERMINAL_STATUSES = new Set(['done', 'delivered', 'accepted', 'archived', 'complete', 'completed']);
80
+ const TERMINAL_STATUSES = new Set(['done', 'delivered', 'canceled', 'accepted', 'archived', 'complete', 'completed']);
81
81
 
82
82
  function isTerminalDeliveredState(state) {
83
83
  if (!state || typeof state !== 'object') return false;
@@ -54,8 +54,10 @@ Machine-readable workflow state lives beside Markdown artifacts in `.kontourai/f
54
54
  | --- | --- | --- |
55
55
  | assignment-provider-settings.schema.json | Flow Agents Assignment Provider Settings | https://flow-agents.dev/schemas/assignment-provider-settings.schema.json |
56
56
  | backlog-provider-settings.schema.json | Flow Agents Backlog Provider Settings | https://flow-agents.dev/schemas/backlog-provider-settings.schema.json |
57
+ | builder-lifecycle-authorization.schema.json | Builder Lifecycle Authorization | https://kontourai.dev/schemas/builder-lifecycle-authorization.schema.json |
57
58
  | decision-record.schema.json | Flow Agents Decision Record | https://flow-agents.dev/schemas/decision-record.schema.json |
58
59
  | flow-agents-settings.schema.json | Flow Agents Settings | https://flow-agents.dev/schemas/flow-agents-settings.schema.json |
60
+ | lifecycle-authority-keys.schema.json | Lifecycle Authority Key Registry | https://kontourai.dev/schemas/lifecycle-authority-keys.schema.json |
59
61
  | workflow-acceptance.schema.json | Flow Agents Workflow Acceptance | https://flow-agents.dev/schemas/workflow-acceptance.schema.json |
60
62
  | workflow-critique.schema.json | Flow Agents Workflow Critique | https://flow-agents.dev/schemas/workflow-critique.schema.json |
61
63
  | workflow-evidence.schema.json | Flow Agents Workflow Evidence | https://flow-agents.dev/schemas/workflow-evidence.schema.json |
@@ -0,0 +1,63 @@
1
+ # Public Workflow CLI
2
+
3
+ Flow Agents exposes its supported consumer workflow surface through the primary package binary.
4
+ Consumer repositories do not need a `package.json`, a local dependency, or a repository-owned
5
+ writer script.
6
+
7
+ Use an exact package version from an isolated npm prefix. This prevents a repository-local
8
+ dependency with the same version from intercepting the command. Generated workflow actions and
9
+ doctor remediation include this isolation automatically:
10
+
11
+ ```bash
12
+ flow_agents() (
13
+ root=$(mktemp -d) || exit 1
14
+ trap 'rm -rf "$root"' EXIT HUP INT TERM
15
+ npm exec --yes --prefix "$root" \
16
+ --package=@kontourai/flow-agents@3.5.0 -- flow-agents "$@"
17
+ )
18
+
19
+ flow_agents workflow start \
20
+ --flow builder.build \
21
+ --work-item owner/repository#123
22
+
23
+ flow_agents workflow status --json
24
+
25
+ flow_agents workflow evidence \
26
+ --expectation implementation-plan \
27
+ --status pass \
28
+ --summary "Implementation plan recorded." \
29
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/example/example--plan.md"}'
30
+ ```
31
+
32
+ The public lifecycle verbs are `pause`, `resume`, `release`, `cancel`, and `archive`. Pause,
33
+ resume, and release require the current assignment actor and an explicit reason. Cancel and
34
+ archive require a signed user/operator authorization file. Flow owns the canonical run
35
+ transition; Flow Agents validates assignment and request binding and projects the resulting run.
36
+
37
+ ```bash
38
+ flow_agents workflow pause --reason "Waiting for a decision"
39
+ flow_agents workflow resume --reason "Decision received"
40
+ flow_agents workflow release --reason "Handing work back"
41
+ flow_agents workflow cancel --authorization-file cancel.json
42
+ flow_agents workflow archive --authorization-file archive.json
43
+ ```
44
+
45
+ `workflow status` is read-only. It reads the actor-scoped current-session pointer, the projected
46
+ state, and the canonical Flow run without rewriting either store.
47
+
48
+ ## Compatibility Doctor
49
+
50
+ Run doctor through the exact isolated package helper defined above:
51
+
52
+ ```bash
53
+ flow_agents workflow doctor --json
54
+ ```
55
+
56
+ The report distinguishes the executing CLI from a repository-local dependency and reports the
57
+ workflow/writer contract, installed hook and writer package, active Kits, Builder Kit content,
58
+ Flow runtime and definition, workflow-state schema, and trust-bundle schema. Incompatible or
59
+ missing installed components produce a nonzero exit and an exact, version-pinned `init` command
60
+ that preserves the recorded runtime and active Kits.
61
+
62
+ The `flow-agents-workflow-sidecar` binary is deprecated and retained only while package-internal
63
+ callers migrate. New consumer guidance must use `flow-agents workflow`.
@@ -118,3 +118,40 @@ Workflow steering surfaces these fields on session start and prompt submission.
118
118
  Stop hook treats an unfinished canonical Flow run as active even during pickup or
119
119
  planning, blocks a premature stop in block mode, and does not release its liveness
120
120
  claim. A run is complete only when Flow reaches its terminal step.
121
+ # Builder Lifecycle Authority
122
+
123
+ The canonical Flow run owns pause, resume, and cancellation. The current assignment actor may
124
+ pause, resume, or release its own assignment with a reason. Cancellation and archival require
125
+ an Ed25519-signed authorization record conforming to
126
+ `schemas/builder-lifecycle-authorization.schema.json`. The record is operation-bound and binds
127
+ the request to the run id, selected Work Item, current assignment actor, immutable external
128
+ request reference, nonce, and expiry. Its signing key must be pinned in the durable
129
+ `.flow-agents/lifecycle-authority-keys.json` registry. Runtime or harness adapters hold the
130
+ private key and capture the signed record from a user/operator channel they trust; agent-authored
131
+ prose or an unsigned model-written file is not cancellation authority.
132
+
133
+ This is an audit and policy boundary, not authentication against a process with unrestricted
134
+ access as the same operating-system user. The harness must keep its signing key outside the
135
+ agent process and enforce its own filesystem or process isolation when the agent is adversarial.
136
+ The repository hooks protect the pinned public-key registry from ordinary agent writes, but are
137
+ explicitly not an operating-system security boundary.
138
+ Adversarial-runtime authentication is tracked separately in Flow Agents issue #545. Flow's
139
+ current lifecycle authority vocabulary also requires agent-owned pause/resume events to use the
140
+ closest available `operator_request` shape; a distinct canonical runtime authority is tracked in
141
+ Flow issue #118.
142
+
143
+ ```text
144
+ flow-agents builder-run pause --session-dir <dir> --reason <text>
145
+ flow-agents builder-run resume --session-dir <dir> --reason <text>
146
+ flow-agents builder-run cancel --session-dir <dir> --authorization-file <record.json>
147
+ flow-agents builder-run release-assignment --session-dir <dir> --reason <text>
148
+ flow-agents builder-run archive --session-dir <dir> --authorization-file <record.json>
149
+ ```
150
+
151
+ Pause and resume verify the live assignment actor under the assignment lock, and preserve the
152
+ current Flow step and assignment. Assignment release does not
153
+ change the Flow run. Cancellation changes Flow first and then idempotently releases the owning
154
+ assignment while holding the same lock; a successfully consumed cancellation nonce cannot be
155
+ replayed. Archive accepts only completed or canceled runs, moves the session under
156
+ `.kontourai/flow-agents/archive/<slug>/`, and retains the canonical Flow run. None of these
157
+ operations deletes a branch or worktree; cleanup requires a separate provider-aware action.
@@ -4,6 +4,11 @@ title: Workflow Usage Guide
4
4
 
5
5
  # Workflow Usage Guide
6
6
 
7
+ > Consumer repositories should use the supported public workflow surface and isolated exact-package
8
+ > launcher documented in [Public Workflow CLI](public-workflow-cli.md). The lower-level writer
9
+ > commands retained later in this guide describe internal/package migration mechanics and are not a
10
+ > consumer prerequisite.
11
+
7
12
  This guide shows how to use the Builder Kit workflow skills in normal chats.
8
13
 
9
14
  > **Which doc do I want?** This page is the *driver's manual* — what to say at each stage and what should happen. If you want the conceptual map first — layers, sidecars, hooks, evidence, and why the system is shaped this way — read the [Agent System Guidebook](agent-system-guidebook.md). For a one-line summary of every skill and gate, use the [Skills Map](skills-map.md). Flow Agents coordinates the local runtime, installs Flow Kits, and records artifacts; Flow owns gate semantics, including typed `expects` entries with `kind: "trust.bundle"`, trusted producer config, and gate overrides.
@@ -41,6 +41,7 @@ CHECKS=(
41
41
  "Flow Kit repository integration|bash evals/integration/test_flow_kit_repository.sh"
42
42
  "Runtime adapter activation integration|bash evals/integration/test_runtime_adapter_activation.sh"
43
43
  "Bundle install integration|bash evals/integration/test_bundle_install.sh"
44
+ "Public workflow CLI integration|bash evals/integration/test_public_workflow_cli.sh"
44
45
  "Bundle lifecycle integration|bash evals/integration/test_bundle_lifecycle.sh"
45
46
  "Activate npx context integration|bash evals/integration/test_activate_npx_context.sh"
46
47
  "Kit conformance levels integration|bash evals/integration/test_kit_conformance_levels.sh"
@@ -133,6 +134,7 @@ LANE_RUNTIME_AND_KIT=(
133
134
  "Flow Kit repository integration"
134
135
  "Runtime adapter activation integration"
135
136
  "Bundle install integration"
137
+ "Public workflow CLI integration"
136
138
  "Bundle lifecycle integration"
137
139
  "Activate npx context integration"
138
140
  "Kit conformance levels integration"
@@ -209,7 +209,7 @@ if (workItem.source_provider?.kind !== 'local' || workItem.source_provider?.path
209
209
  if (flowState.current_step !== 'design-probe' || flowState.subject !== 'local:local-request') process.exit(1);
210
210
  if (state.flow_run?.current_step !== 'design-probe') process.exit(1);
211
211
  if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
212
- if (!state.next_action?.command?.includes('builder-run sync')) process.exit(1);
212
+ if (!state.next_action?.command?.includes("'workflow' 'status'")) process.exit(1);
213
213
  if ('enforcement' in state.next_action) process.exit(1);
214
214
  const bundle = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'trust.bundle'), 'utf8'));
215
215
  const selected = (bundle.claims || []).find((claim) => claim.claimType === 'builder.pull-work.selected');
@@ -247,7 +247,7 @@ const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'ut
247
247
  if (flowState.current_step !== 'design-probe' || flowState.subject !== 'local:local-request') process.exit(1);
248
248
  if (sidecar.flow_run?.current_step !== 'design-probe') process.exit(1);
249
249
  if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
250
- if (!sidecar.next_action?.command?.includes('builder-run sync')) process.exit(1);
250
+ if (!sidecar.next_action?.command?.includes("'workflow' 'status'")) process.exit(1);
251
251
  NODE
252
252
  then
253
253
  pass "repeated ensure-session loads the canonical Flow run without resetting its history"
@@ -275,7 +275,7 @@ elif [[ -f "$BROKEN_ROOT/broken-start/state.json" ]] \
275
275
  const fs = require('node:fs');
276
276
  const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
277
277
  if (state.flow_run) process.exit(1);
278
- if (!state.next_action?.command?.includes('builder-run start')) process.exit(1);
278
+ if (!state.next_action?.command?.includes("'workflow' 'start'")) process.exit(1);
279
279
  NODE
280
280
  then
281
281
  pass "failed canonical startup is visible and leaves only retryable sidecar guidance"
@@ -482,12 +482,13 @@ else
482
482
  stop_status=$?
483
483
  if [[ "$stop_status" -eq 2 ]] \
484
484
  && grep -q 'required skills: pickup-probe' "$TMP/stop.err" \
485
- && grep -q 'builder-run sync' "$TMP/stop.err" \
485
+ && grep -q 'next command: sh -c' "$TMP/stop.err" \
486
486
  && grep -q 'release skipped for active Flow run' "$TMP/stop.err" \
487
487
  && node - "$LOCAL_SESSION/state.json" <<'NODE'
488
488
  const fs = require('node:fs');
489
489
  const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
490
490
  if (state.flow_run?.status !== 'active' || state.flow_run?.current_step !== 'design-probe') process.exit(1);
491
+ if (!state.next_action?.command?.includes("'workflow' 'status'")) process.exit(1);
491
492
  NODE
492
493
  then
493
494
  pass "active Flow run blocks Stop, preserves liveness, and exposes executable guidance"
@@ -896,18 +896,19 @@ cat >"$PACKAGE_AMBIENT/kits/builder/flows/build.flow.json" <<'JSON'
896
896
  }
897
897
  JSON
898
898
  PACKAGE_PACK_LOG="$TMPDIR_EVAL/package-pack.log"
899
+ PACKAGE_CLI="$PACKAGE_CONSUMER/node_modules/@kontourai/flow-agents/build/src/cli.js"
900
+ PACKAGE_SESSION="$PACKAGE_PROJECT/.kontourai/flow-agents/packed-builder-entry"
899
901
  if (cd "$ROOT_DIR" && npm pack --silent --pack-destination "$TMPDIR_EVAL" >"$PACKAGE_PACK_LOG") \
900
902
  && PACKAGE_TARBALL="$(find "$TMPDIR_EVAL" -maxdepth 1 -type f -name 'kontourai-flow-agents-*.tgz' -print -quit)" \
901
903
  && [[ -n "$PACKAGE_TARBALL" ]] \
902
904
  && 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 \
905
+ && (cd "$PACKAGE_AMBIENT" && CODEX_SESSION_ID=packed-package-consumer node "$PACKAGE_CONSUMER/node_modules/@kontourai/flow-agents/build/src/cli/workflow-sidecar.js" ensure-session \
904
906
  --artifact-root "$PACKAGE_PROJECT/.kontourai/flow-agents" \
905
907
  --task-slug packed-builder-entry \
906
- --actor packed-package-consumer \
907
908
  --title "Packed Builder entry" \
908
909
  --summary "Installed package should project pickup-probe." \
909
910
  --flow-id builder.build >/dev/null 2>&1) \
910
- && node - "$PACKAGE_PROJECT" <<'NODE'
911
+ && node - "$PACKAGE_PROJECT" <<'NODE' &&
911
912
  const fs = require('node:fs');
912
913
  const path = require('node:path');
913
914
  const project = process.argv[2];
@@ -917,11 +918,64 @@ const bundle = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow
917
918
  if (flow.current_step !== 'design-probe' || state.flow_run?.current_step !== 'design-probe') process.exit(1);
918
919
  if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
919
920
  if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) process.exit(1);
921
+ NODE
922
+ node --input-type=module - "$PACKAGE_PROJECT" "$PACKAGE_CONSUMER" <<'NODE' &&
923
+ import fs from 'node:fs';
924
+ import path from 'node:path';
925
+ import { generateKeyPairSync, sign } from 'node:crypto';
926
+ import { pathToFileURL } from 'node:url';
927
+ const [project, consumer] = process.argv.slice(2);
928
+ const slug = 'packed-builder-entry';
929
+ const assignment = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'assignment', `${slug}.json`), 'utf8'));
930
+ const state = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', slug, 'state.json'), 'utf8'));
931
+ const packageEntry = path.join(consumer, 'node_modules', '@kontourai', 'flow-agents', 'build', 'src', 'index.js');
932
+ const { builderLifecycleAuthorizationPayload } = await import(pathToFileURL(packageEntry).href);
933
+ const keys = generateKeyPairSync('ed25519');
934
+ const keyId = 'packed-consumer';
935
+ fs.mkdirSync(path.join(project, '.flow-agents'), { recursive: true });
936
+ fs.writeFileSync(path.join(project, '.flow-agents', 'lifecycle-authority-keys.json'), JSON.stringify({
937
+ schema_version: '1.0',
938
+ keys: [{ id: keyId, algorithm: 'ed25519', public_key_pem: keys.publicKey.export({ type: 'spki', format: 'pem' }) }],
939
+ }, null, 2));
940
+ for (const operation of ['cancel', 'archive']) {
941
+ const requestedAt = new Date();
942
+ const unsigned = {
943
+ schema_version: '1.0',
944
+ operation,
945
+ run_id: slug,
946
+ subject: state.work_item_refs[0],
947
+ assignment_actor_key: assignment.actor_key,
948
+ assignment_actor: { ...assignment.actor, human: assignment.actor.human ?? null },
949
+ nonce: `packed-${operation}`,
950
+ expires_at: new Date(requestedAt.getTime() + 60 * 60_000).toISOString(),
951
+ request: {
952
+ reason: `packed consumer ${operation}`,
953
+ authority: { kind: 'user_request', actor: 'packed-fixture', request_ref: `fixture://packed/${operation}`, requested_at: requestedAt.toISOString() },
954
+ },
955
+ };
956
+ const authorization = { ...unsigned, signature: { algorithm: 'ed25519', key_id: keyId, value: sign(null, Buffer.from(builderLifecycleAuthorizationPayload(unsigned)), keys.privateKey).toString('base64') } };
957
+ fs.writeFileSync(path.join(project, `${operation}.authorization.json`), JSON.stringify(authorization, null, 2));
958
+ }
959
+ NODE
960
+ (cd "$PACKAGE_PROJECT" && CODEX_SESSION_ID=packed-package-consumer node "$PACKAGE_CLI" builder-run pause --session-dir "$PACKAGE_SESSION" --reason "packed pause" >/dev/null) \
961
+ && (cd "$PACKAGE_PROJECT" && CODEX_SESSION_ID=packed-package-consumer node "$PACKAGE_CLI" builder-run resume --session-dir "$PACKAGE_SESSION" --reason "packed resume" >/dev/null) \
962
+ && (cd "$PACKAGE_PROJECT" && node "$PACKAGE_CLI" builder-run cancel --session-dir "$PACKAGE_SESSION" --authorization-file "$PACKAGE_PROJECT/cancel.authorization.json" >/dev/null) \
963
+ && (cd "$PACKAGE_PROJECT" && node "$PACKAGE_CLI" builder-run archive --session-dir "$PACKAGE_SESSION" --authorization-file "$PACKAGE_PROJECT/archive.authorization.json" >/dev/null) \
964
+ && node - "$PACKAGE_PROJECT" <<'NODE'
965
+ const fs = require('node:fs');
966
+ const path = require('node:path');
967
+ const project = process.argv[2];
968
+ const slug = 'packed-builder-entry';
969
+ const archived = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'archive', slug, 'state.json'), 'utf8'));
970
+ const flow = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', slug, 'state.json'), 'utf8'));
971
+ const consumed = fs.readdirSync(path.join(project, '.kontourai', 'flow-agents', 'lifecycle-authority', 'consumed'));
972
+ if (archived.status !== 'archived' || flow.status !== 'canceled' || consumed.length !== 2) process.exit(1);
973
+ if (fs.existsSync(path.join(project, '.kontourai', 'flow-agents', slug))) process.exit(1);
920
974
  NODE
921
975
  then
922
- _pass "packed npm consumer ignores unrelated ambient Flow definitions and projects pickup-probe"
976
+ _pass "packed npm consumer projects Builder entry and executes lifecycle commands"
923
977
  else
924
- _fail "packed npm consumer did not execute canonical Builder entry"
978
+ _fail "packed npm consumer did not execute canonical Builder entry and lifecycle commands"
925
979
  fi
926
980
 
927
981
  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
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
+ TMP="$(mktemp -d)"
6
+ trap 'rm -rf "$TMP"' EXIT
7
+
8
+ fail() { printf 'FAIL %s\n' "$*" >&2; exit 1; }
9
+ pass() { printf 'PASS %s\n' "$*"; }
10
+
11
+ cd "$ROOT_DIR"
12
+ npm run build --silent
13
+ npm run build:bundles --silent
14
+ npm pack --silent --pack-destination "$TMP" >/dev/null
15
+ TARBALL="$(find "$TMP" -maxdepth 1 -name 'kontourai-flow-agents-*.tgz' -print -quit)"
16
+ [[ -n "$TARBALL" ]] || fail "npm pack did not produce a tarball"
17
+ VERSION="$(node -p "require('./package.json').version")"
18
+ CONSUMER="$TMP/consumer"
19
+ ARTIFACT_ROOT="$CONSUMER/.kontourai/flow-agents"
20
+ mkdir -p "$CONSUMER"
21
+
22
+ run_candidate() {
23
+ (cd "$CONSUMER" && CODEX_SESSION_ID=public-workflow-eval npx --yes --package="file:$TARBALL" flow-agents workflow "$@")
24
+ }
25
+
26
+ PRIMARY_HELP="$(cd "$CONSUMER" && npx --yes --package="file:$TARBALL" flow-agents --help)"
27
+ WORKFLOW_HELP="$(run_candidate --help)"
28
+ [[ "$PRIMARY_HELP" == *"workflow"* && "$WORKFLOW_HELP" != *"workflow-sidecar"* && "$WORKFLOW_HELP" != *"npm run workflow:sidecar"* ]] || fail "public help exposes internal writer terminology or omits workflow"
29
+ pass "primary help exposes the public workflow command without internal writer terminology"
30
+
31
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#101 --summary "Release fixture" >/dev/null
32
+ RELEASE_SESSION="$ARTIFACT_ROOT/acme-widgets-101"
33
+ [[ -f "$RELEASE_SESSION/state.json" ]] || fail "packed start did not create a session"
34
+ [[ ! -e "$CONSUMER/package.json" ]] || fail "consumer unexpectedly gained package.json"
35
+ pass "packed start works in a non-Node consumer"
36
+ 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)"
38
+ UNSAFE_START_RC=$?
39
+ set -e
40
+ [[ "$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"
41
+ pass "public start rejects internal authority flags before mutation"
42
+
43
+ LOCAL_RETRY_PROJECT="$TMP/local-retry-project"
44
+ LOCAL_RETRY_ROOT="$LOCAL_RETRY_PROJECT/.kontourai/flow-agents"
45
+ mkdir -p "$LOCAL_RETRY_PROJECT/.kontourai"
46
+ printf 'not a run-store directory\n' >"$LOCAL_RETRY_PROJECT/.kontourai/flow"
47
+ set +e
48
+ (cd "$LOCAL_RETRY_PROJECT" && CODEX_SESSION_ID=public-workflow-eval npx --yes --package="file:$TARBALL" flow-agents-workflow-sidecar ensure-session \
49
+ --artifact-root "$LOCAL_RETRY_ROOT" --task-slug local-retry \
50
+ --title "Local retry" --summary "Resume the bound local workflow." --flow-id builder.build >/dev/null 2>&1)
51
+ LOCAL_SEED_RC=$?
52
+ set -e
53
+ [[ "$LOCAL_SEED_RC" -ne 0 ]] || fail "local retry fixture unexpectedly started against an invalid Flow store"
54
+ LOCAL_RETRY_COMMAND="$(node -p "JSON.parse(require('fs').readFileSync('$LOCAL_RETRY_ROOT/local-retry/state.json')).next_action.command")"
55
+ [[ "$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
+ rm -f "$LOCAL_RETRY_PROJECT/.kontourai/flow"
57
+ FOREIGN_RETRY_CWD="$TMP/foreign-retry-cwd"
58
+ mkdir -p "$FOREIGN_RETRY_CWD"
59
+ EXECUTABLE_RETRY="$(node -e 'process.stdout.write(process.argv[1].replace(process.argv[2], process.argv[3]))' "$LOCAL_RETRY_COMMAND" "'@kontourai/flow-agents@$VERSION'" "'file:$TARBALL'")"
60
+ (cd "$FOREIGN_RETRY_CWD" && CODEX_SESSION_ID=public-workflow-eval eval "$EXECUTABLE_RETRY" >/dev/null)
61
+ [[ -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
+ pass "emitted local retry executes from a foreign cwd against its exact originating store"
63
+
64
+ snapshot_tree() {
65
+ local root="$1"
66
+ find "$root" -type f -print0 | sort -z | xargs -0 shasum -a 256
67
+ }
68
+ BEFORE_STATUS="$(snapshot_tree "$CONSUMER/.kontourai")"
69
+ STATUS_JSON="$(run_candidate status --artifact-root "$ARTIFACT_ROOT" --json)"
70
+ AFTER_STATUS="$(snapshot_tree "$CONSUMER/.kontourai")"
71
+ [[ "$BEFORE_STATUS" == "$AFTER_STATUS" ]] || fail "workflow status mutated durable artifacts"
72
+ 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"
73
+ pass "status is canonical and byte-read-only"
74
+
75
+ FLOW_MANIFEST="$CONSUMER/.kontourai/flow/runs/acme-widgets-101/evidence/manifest.json"
76
+ 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
78
+ 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"
81
+
82
+ OUTSIDE="$TMP/outside-session"
83
+ mkdir -p "$OUTSIDE"
84
+ printf '{"schema_version":"1.0","task_slug":"outside"}\n' >"$OUTSIDE/state.json"
85
+ ln -s "$OUTSIDE" "$ARTIFACT_ROOT/symlink-session"
86
+ OUTSIDE_BEFORE="$(snapshot_tree "$OUTSIDE")"
87
+ set +e
88
+ SYMLINK_EVIDENCE="$(run_candidate evidence --session-dir "$ARTIFACT_ROOT/symlink-session" --expectation pickup-probe-readiness --status not_verified --summary rejected 2>&1)"
89
+ SYMLINK_RC=$?
90
+ set -e
91
+ [[ "$SYMLINK_RC" -ne 0 && "$SYMLINK_EVIDENCE" == *"session directory must be a non-symlink directory"* && "$(snapshot_tree "$OUTSIDE")" == "$OUTSIDE_BEFORE" ]] || fail "evidence followed a symlinked session"
92
+ pass "evidence rejects symlinked session paths before mutation"
93
+
94
+ run_candidate pause --session-dir "$RELEASE_SESSION" --reason "consumer pause" >/dev/null
95
+ run_candidate resume --session-dir "$RELEASE_SESSION" --reason "consumer resume" >/dev/null
96
+ run_candidate release --session-dir "$RELEASE_SESSION" --reason "consumer release" >/dev/null
97
+ 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
+ pass "pause, resume, and release use the public command"
99
+
100
+ run_candidate start --artifact-root "$ARTIFACT_ROOT" --flow builder.build --work-item acme/widgets#102 --summary "Cancel fixture" >/dev/null
101
+ CANCEL_SESSION="$ARTIFACT_ROOT/acme-widgets-102"
102
+ node --input-type=module - "$CONSUMER" "$CANCEL_SESSION" <<'NODE'
103
+ import fs from 'node:fs';
104
+ import path from 'node:path';
105
+ import { generateKeyPairSync, sign } from 'node:crypto';
106
+ const [project, session] = process.argv.slice(2);
107
+ const slug = path.basename(session);
108
+ const assignment = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'assignment', `${slug}.json`), 'utf8'));
109
+ const state = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
110
+ const keys = generateKeyPairSync('ed25519');
111
+ fs.mkdirSync(path.join(project, '.flow-agents'), { recursive: true });
112
+ fs.writeFileSync(path.join(project, '.flow-agents', 'lifecycle-authority-keys.json'), JSON.stringify({ schema_version: '1.0', keys: [{ id: 'consumer', algorithm: 'ed25519', public_key_pem: keys.publicKey.export({ type: 'spki', format: 'pem' }) }] }, null, 2));
113
+ for (const operation of ['cancel', 'archive']) {
114
+ const requestedAt = new Date();
115
+ const unsigned = {
116
+ schema_version: '1.0', operation, run_id: slug, subject: state.work_item_refs[0],
117
+ assignment_actor_key: assignment.actor_key,
118
+ assignment_actor: { ...assignment.actor, human: assignment.actor.human ?? null },
119
+ nonce: `consumer-${operation}`,
120
+ expires_at: new Date(requestedAt.getTime() + 3600000).toISOString(),
121
+ request: { reason: `consumer ${operation}`, authority: { kind: 'user_request', actor: 'consumer-user', request_ref: `fixture://consumer/${operation}`, requested_at: requestedAt.toISOString() } },
122
+ };
123
+ const authorization = { ...unsigned, signature: { algorithm: 'ed25519', key_id: 'consumer', value: sign(null, Buffer.from(JSON.stringify(unsigned)), keys.privateKey).toString('base64') } };
124
+ fs.writeFileSync(path.join(project, `${operation}.authorization.json`), JSON.stringify(authorization, null, 2));
125
+ }
126
+ NODE
127
+ run_candidate cancel --session-dir "$CANCEL_SESSION" --authorization-file "$CONSUMER/cancel.authorization.json" >/dev/null
128
+ run_candidate archive --session-dir "$CANCEL_SESSION" --authorization-file "$CONSUMER/archive.authorization.json" >/dev/null
129
+ [[ -f "$ARTIFACT_ROOT/archive/acme-widgets-102/state.json" && ! -e "$CANCEL_SESSION" ]] || fail "cancel/archive did not retain archived session"
130
+ pass "signed cancel and archive execute through the public command"
131
+
132
+ DOCTOR="$TMP/doctor-consumer"
133
+ 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
+ cat >"$DOCTOR/node_modules/@kontourai/flow-agents/package.json" <<'JSON'
135
+ {"name":"@kontourai/flow-agents","version":"3.4.3","bin":{"flow-agents":"bin.js"}}
136
+ JSON
137
+ cat >"$DOCTOR/node_modules/.bin/flow-agents" <<'SH'
138
+ #!/usr/bin/env bash
139
+ echo STALE_LOCAL_BINARY
140
+ SH
141
+ chmod +x "$DOCTOR/node_modules/.bin/flow-agents"
142
+ cat >"$DOCTOR/.flow-agents/install.json" <<'JSON'
143
+ {"version":"3.4.3","runtime":"codex","active_kit_ids":["builder"]}
144
+ JSON
145
+ cat >"$DOCTOR/kits/builder/kit.json" <<'JSON'
146
+ {"schema_version":"0.9","id":"builder"}
147
+ JSON
148
+ cat >"$DOCTOR/kits/builder/flows/build.flow.json" <<'JSON'
149
+ {"id":"builder.build","version":"0.9"}
150
+ JSON
151
+ cat >"$DOCTOR/.kontourai/flow-agents/current.json" <<'JSON'
152
+ {"active_slug":"doctor-session","artifact_dir":"doctor-session"}
153
+ JSON
154
+ cat >"$DOCTOR/.kontourai/flow-agents/doctor-session/state.json" <<'JSON'
155
+ {"schema_version":"0.9","task_slug":"doctor-session","flow_run":{"definition_id":"builder.build","definition_version":"0.9"}}
156
+ JSON
157
+ cat >"$DOCTOR/.kontourai/flow-agents/doctor-session/trust.bundle" <<'JSON'
158
+ {"schema_version":"0.9"}
159
+ JSON
160
+ set +e
161
+ DOCTOR_JSON="$(cd "$DOCTOR" && npx --yes --package="file:$TARBALL" flow-agents workflow doctor --project-root "$DOCTOR" --artifact-root "$DOCTOR/.kontourai/flow-agents" --json 2>/dev/null)"
162
+ DOCTOR_RC=$?
163
+ set -e
164
+ [[ "$DOCTOR_RC" -eq 2 ]] || fail "doctor should return 2 for incompatible consumer fixtures"
165
+ node - "$DOCTOR_JSON" "$VERSION" "$DOCTOR" <<'NODE'
166
+ const [reportText, version, root] = process.argv.slice(2);
167
+ const report = JSON.parse(reportText);
168
+ if (report.cli.version !== version || report.cli.workflow_contract_version !== '1.0') process.exit(1);
169
+ if (report.local_dependency.version !== '3.4.3' || report.local_dependency.selected !== false) process.exit(2);
170
+ if (!report.warnings.some((w) => w.includes('hook/writer version 3.4.3'))) process.exit(3);
171
+ if (!report.warnings.some((w) => w.includes('Builder Kit'))) process.exit(4);
172
+ if (!report.warnings.some((w) => w.includes('builder.build version 0.9'))) process.exit(5);
173
+ if (!report.warnings.some((w) => w.includes('Artifact schema 0.9'))) process.exit(6);
174
+ if (!report.warnings.some((w) => w.includes('Trust bundle schema 0.9'))) process.exit(7);
175
+ if (!report.warnings.some((w) => w.includes('hook/writer assets failed integrity'))) process.exit(10);
176
+ if (!report.remediation.startsWith('sh -c ') || !report.remediation.includes(`'@kontourai/flow-agents@${version}'`) || !report.remediation.includes("'--runtime' 'codex'") || !report.remediation.includes("'--activate-kit' 'builder'")) process.exit(8);
177
+ if (report.cli.package_root.startsWith(root)) process.exit(9);
178
+ NODE
179
+ pass "doctor detects same-major hook/writer, Kit, Flow, and schema skew with exact remediation"
180
+ [[ "$DOCTOR_JSON" != *"STALE_LOCAL_BINARY"* ]] || fail "explicit package invocation selected stale local binary"
181
+ pass "explicit packed package wins over an old local dependency"
182
+
183
+ node - "$DOCTOR/node_modules/@kontourai/flow-agents/package.json" "$VERSION" <<'NODE'
184
+ const fs = require('node:fs');
185
+ const [file, version] = process.argv.slice(2);
186
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
187
+ value.version = version;
188
+ fs.writeFileSync(file, JSON.stringify(value));
189
+ NODE
190
+ SAME_VERSION_HELP="$(cd "$DOCTOR" && npx --yes --package="file:$TARBALL" flow-agents --help)"
191
+ [[ "$SAME_VERSION_HELP" == *"workflow"* && "$SAME_VERSION_HELP" != *"STALE_LOCAL_BINARY"* ]] || fail "explicit tarball selected a hostile same-version local binary"
192
+ pass "explicit tarball wins over a hostile same-version local binary"
193
+
194
+ PINNED_COMMAND_MODULE="pinned-cli-command.js"
195
+ ISOLATED_COMMAND="$(node --input-type=module - "$ROOT_DIR/build/src/lib/$PINNED_COMMAND_MODULE" "file:$TARBALL" <<'NODE'
196
+ import { pathToFileURL } from 'node:url';
197
+ const [modulePath, packageSpec] = process.argv.slice(2);
198
+ const { isolatedPackageCommand } = await import(pathToFileURL(modulePath));
199
+ console.log(isolatedPackageCommand(packageSpec, 'flow-agents', ['--help']));
200
+ NODE
201
+ )"
202
+ ISOLATED_HELP="$(cd "$DOCTOR" && eval "$ISOLATED_COMMAND")"
203
+ [[ "$ISOLATED_HELP" == *"workflow"* && "$ISOLATED_HELP" != *"STALE_LOCAL_BINARY"* ]] || fail "isolated package command selected a hostile same-version local binary"
204
+ pass "generated isolated package command defeats a hostile same-version local binary"
205
+
206
+ rm -f "$DOCTOR/kits/builder/kit.json" "$DOCTOR/kits/builder/flows/build.flow.json"
207
+ printf '{"version":"%s","runtime":"codex","active_kit_ids":["builder"]}\n' "$VERSION" >"$DOCTOR/.flow-agents/install.json"
208
+ set +e
209
+ MISSING_JSON="$(cd "$DOCTOR" && npx --yes --package="file:$TARBALL" flow-agents workflow doctor --project-root "$DOCTOR" --artifact-root "$DOCTOR/.kontourai/flow-agents" --json 2>/dev/null)"
210
+ MISSING_RC=$?
211
+ set -e
212
+ [[ "$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"
214
+ pass "doctor fails closed for missing activated Kit components"
215
+
216
+ HEALTHY="$TMP/healthy-install"
217
+ mkdir -p "$HEALTHY"
218
+ (cd "$HEALTHY" && npx --yes --package="file:$TARBALL" flow-agents init --runtime codex --dest "$HEALTHY" --activate-kit builder --yes >/dev/null)
219
+ HEALTHY_JSON="$(cd "$HEALTHY" && npx --yes --package="file:$TARBALL" flow-agents workflow doctor --project-root "$HEALTHY" --artifact-root "$HEALTHY/.kontourai/flow-agents" --json)"
220
+ node -e 'const r=JSON.parse(process.argv[1]);if(!r.ok||r.warnings.length||!r.hook.integrity.ok||r.installed.active_kit_ids[0]!=="builder")process.exit(1)' "$HEALTHY_JSON" || fail "doctor did not pass immediately after its own remediation install"
221
+ pass "real init converges to doctor PASS"
222
+
223
+ cp "$HEALTHY/build/src/cli/workflow.js" "$TMP/workflow.js.clean"
224
+ printf '\n// WORKFLOW_CONTRACT_VERSION = "1.0"\n' >>"$HEALTHY/build/src/cli/workflow.js"
225
+ set +e
226
+ TAMPERED_CLI_JSON="$(cd "$HEALTHY" && npx --yes --package="file:$TARBALL" flow-agents workflow doctor --project-root "$HEALTHY" --artifact-root "$HEALTHY/.kontourai/flow-agents" --json 2>/dev/null)"
227
+ TAMPERED_CLI_RC=$?
228
+ set -e
229
+ [[ "$TAMPERED_CLI_RC" -eq 2 && "$TAMPERED_CLI_JSON" == *"asset mismatch: build/src/cli/workflow.js"* ]] || fail "doctor accepted marker-preserving CLI tampering"
230
+ cp "$TMP/workflow.js.clean" "$HEALTHY/build/src/cli/workflow.js"
231
+ pass "doctor rejects marker-preserving CLI tampering"
232
+
233
+ cp "$HEALTHY/.codex/hooks.json" "$TMP/hooks.json.clean"
234
+ node - "$HEALTHY/.codex/hooks.json" <<'NODE'
235
+ const fs = require('node:fs');
236
+ const file = process.argv[2];
237
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
238
+ const group = value.hooks.SessionStart.find((entry) => JSON.stringify(entry).includes('workflow-steering'));
239
+ group.hooks[0].command += '; true';
240
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
241
+ NODE
242
+ set +e
243
+ TAMPERED_HOOK_JSON="$(cd "$HEALTHY" && npx --yes --package="file:$TARBALL" flow-agents workflow doctor --project-root "$HEALTHY" --artifact-root "$HEALTHY/.kontourai/flow-agents" --json 2>/dev/null)"
244
+ TAMPERED_HOOK_RC=$?
245
+ set -e
246
+ [[ "$TAMPERED_HOOK_RC" -eq 2 && "$TAMPERED_HOOK_JSON" == *"does not contain the packaged managed hooks"* ]] || fail "doctor accepted name-preserving hook tampering"
247
+ cp "$TMP/hooks.json.clean" "$HEALTHY/.codex/hooks.json"
248
+ pass "doctor rejects name-preserving hook configuration tampering"
249
+
250
+ for RUNTIME in base claude-code opencode pi kiro; do
251
+ RUNTIME_ROOT="$TMP/runtime-$RUNTIME"
252
+ mkdir -p "$RUNTIME_ROOT"
253
+ (cd "$RUNTIME_ROOT" && npx --yes --package="file:$TARBALL" flow-agents init --runtime "$RUNTIME" --dest "$RUNTIME_ROOT" --activate-kit builder --yes >/dev/null)
254
+ RUNTIME_JSON="$(cd "$RUNTIME_ROOT" && npx --yes --package="file:$TARBALL" flow-agents workflow doctor --project-root "$RUNTIME_ROOT" --artifact-root "$RUNTIME_ROOT/.kontourai/flow-agents" --json)"
255
+ node -e 'const r=JSON.parse(process.argv[1]);if(!r.ok||r.warnings.length||!r.hook.integrity.ok)process.exit(1)' "$RUNTIME_JSON" || fail "doctor did not validate $RUNTIME runtime wiring"
256
+ pass "doctor validates $RUNTIME runtime wiring"
257
+ done
258
+
259
+ printf 'public workflow CLI integration passed\n'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.5.0",
3
+ "version": "3.6.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",
@@ -149,7 +149,7 @@
149
149
  "typescript": "^6.0.3"
150
150
  },
151
151
  "dependencies": {
152
- "@kontourai/flow": "^3.0.0"
152
+ "@kontourai/flow": "^3.1.0"
153
153
  },
154
154
  "optionalDependencies": {
155
155
  "@kontourai/surface": "^2.0.0",