@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.
- package/.github/workflows/ci.yml +4 -0
- package/CHANGELOG.md +8 -0
- package/build/src/builder-flow-run-adapter.d.ts +10 -1
- package/build/src/builder-flow-run-adapter.js +29 -1
- package/build/src/builder-flow-runtime.d.ts +18 -0
- package/build/src/builder-flow-runtime.js +205 -13
- package/build/src/builder-lifecycle-authority.d.ts +35 -0
- package/build/src/builder-lifecycle-authority.js +219 -0
- package/build/src/cli/assignment-provider.d.ts +10 -0
- package/build/src/cli/assignment-provider.js +61 -52
- package/build/src/cli/builder-run.js +46 -5
- package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
- package/build/src/cli/workflow-sidecar.d.ts +3 -0
- package/build/src/cli/workflow-sidecar.js +28 -6
- package/build/src/cli/workflow.d.ts +2 -0
- package/build/src/cli/workflow.js +521 -0
- package/build/src/cli.js +2 -0
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +2 -0
- package/build/src/lib/package-version.d.ts +2 -0
- package/build/src/lib/package-version.js +13 -0
- package/build/src/lib/pinned-cli-command.d.ts +6 -0
- package/build/src/lib/pinned-cli-command.js +21 -0
- package/context/contracts/artifact-contract.md +1 -1
- package/context/scripts/hooks/config-protection.js +8 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/context/scripts/hooks/stop-goal-fit.js +1 -1
- package/docs/context-map.md +2 -0
- package/docs/public-workflow-cli.md +63 -0
- package/docs/spec/builder-flow-runtime.md +37 -0
- package/docs/workflow-usage-guide.md +5 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -4
- package/evals/integration/test_bundle_install.sh +59 -5
- package/evals/integration/test_public_workflow_cli.sh +259 -0
- package/package.json +2 -2
- package/schemas/builder-lifecycle-authorization.schema.json +57 -0
- package/schemas/lifecycle-authority-keys.schema.json +25 -0
- package/schemas/workflow-state.schema.json +1 -1
- package/scripts/hooks/config-protection.js +8 -1
- package/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/scripts/hooks/stop-goal-fit.js +1 -1
- package/src/builder-flow-run-adapter.ts +47 -0
- package/src/builder-flow-runtime.ts +216 -4
- package/src/builder-lifecycle-authority.ts +218 -0
- package/src/cli/assignment-provider.ts +29 -9
- package/src/cli/builder-flow-runtime.test.mjs +404 -1
- package/src/cli/builder-run.ts +56 -5
- package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
- package/src/cli/workflow-sidecar.ts +28 -6
- package/src/cli/workflow.ts +471 -0
- package/src/cli.ts +2 -0
- package/src/index.ts +14 -0
- package/src/lib/package-version.ts +15 -0
- 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;
|
package/docs/context-map.md
CHANGED
|
@@ -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.
|
package/evals/ci/run-baseline.sh
CHANGED
|
@@ -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('
|
|
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('
|
|
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('
|
|
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 '
|
|
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
|
|
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.
|
|
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.
|
|
152
|
+
"@kontourai/flow": "^3.1.0"
|
|
153
153
|
},
|
|
154
154
|
"optionalDependencies": {
|
|
155
155
|
"@kontourai/surface": "^2.0.0",
|