@kontourai/flow-agents 1.4.0 → 2.0.1
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/CODEOWNERS +29 -0
- package/.github/actions/trust-verify/action.yml +145 -0
- package/.github/workflows/ci.yml +11 -4
- package/.github/workflows/kit-gates-demo.yml +2 -2
- package/.github/workflows/publish-npm.yml +10 -2
- package/.github/workflows/release-please.yml +1 -1
- package/.github/workflows/runtime-compat.yml +1 -1
- package/.github/workflows/trust-reconcile.yml +113 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +103 -0
- package/CONTRIBUTING.md +4 -4
- package/README.md +1 -0
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/init.js +242 -20
- package/build/src/cli/validate-workflow-artifacts.js +19 -2
- package/build/src/cli/verify.d.ts +1 -0
- package/build/src/cli/verify.js +90 -0
- package/build/src/cli/workflow-sidecar.d.ts +316 -8
- package/build/src/cli/workflow-sidecar.js +1996 -91
- package/build/src/cli.js +2 -3
- package/build/src/lib/flow-resolver.d.ts +111 -0
- package/build/src/lib/flow-resolver.js +308 -0
- package/build/src/tools/build-universal-bundles.js +34 -22
- package/build/src/tools/generate-context-map.js +3 -16
- package/build/src/tools/validate-source-tree.d.ts +1 -1
- package/build/src/tools/validate-source-tree.js +42 -162
- package/context/contracts/artifact-contract.md +10 -0
- package/context/contracts/delivery-contract.md +1 -0
- package/context/contracts/review-contract.md +1 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/gate-awareness.md +39 -0
- package/context/scripts/hooks/stop-goal-fit.js +632 -70
- package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
- package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0007-skill-audit.md +1 -1
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
- package/docs/adr/0011-mcp-posture.md +100 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
- package/docs/adr/0013-context-lifecycle.md +151 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
- package/docs/adr/0016-three-hard-boundary-model.md +71 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
- package/docs/agent-system-guidebook.md +5 -12
- package/docs/context-map.md +4 -10
- package/docs/index.md +3 -2
- package/docs/integrations/framework-adapter.md +19 -6
- package/docs/integrations/index.md +2 -2
- package/docs/north-star.md +4 -4
- package/docs/operating-layers.md +3 -3
- package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
- package/docs/repository-structure.md +2 -2
- package/docs/skills-map.md +1 -0
- package/docs/spec/runtime-hook-surface.md +62 -9
- package/docs/standards-register.md +3 -3
- package/docs/survey-utterance-check.md +1 -1
- package/docs/trust-anchor-adoption.md +197 -0
- package/docs/verifiable-trust.md +95 -0
- package/docs/veritas-integration.md +2 -2
- package/docs/workflow-usage-guide.md +69 -0
- package/evals/acceptance/DEMO-false-completion.md +144 -0
- package/evals/acceptance/demo-cast.sh +92 -0
- package/evals/acceptance/demo-false-completion.sh +72 -0
- package/evals/acceptance/demo-real-evidence.sh +104 -0
- package/evals/acceptance/demo.tape +29 -0
- package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
- package/evals/acceptance/prove-capture-teeth.sh +114 -0
- package/evals/acceptance/prove-teeth.sh +105 -0
- package/evals/ci/antigaming-suite.sh +55 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
- package/evals/integration/test_builder_step_producers.sh +379 -0
- package/evals/integration/test_bundle_install.sh +35 -71
- package/evals/integration/test_bundle_lifecycle.sh +39 -2
- package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
- package/evals/integration/test_checkpoint_signing.sh +489 -0
- package/evals/integration/test_claim_lookup.sh +352 -0
- package/evals/integration/test_command_log_fork_classification.sh +134 -0
- package/evals/integration/test_command_log_integrity.sh +275 -0
- package/evals/integration/test_context_map.sh +0 -2
- package/evals/integration/test_dual_emit_flow_step.sh +278 -0
- package/evals/integration/test_enforcer_expects_driven.sh +281 -0
- package/evals/integration/test_evidence_capture_hook.sh +185 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +273 -0
- package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
- package/evals/integration/test_gate_bypass_chain.sh +448 -0
- package/evals/integration/test_gate_lockdown.sh +1137 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
- package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
- package/evals/integration/test_goal_fit_hook.sh +69 -4
- package/evals/integration/test_goal_fit_rederive.sh +263 -0
- package/evals/integration/test_install_merge.sh +1176 -0
- package/evals/integration/test_kit_identity_trust.sh +393 -0
- package/evals/integration/test_mint_attestation.sh +373 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
- package/evals/integration/test_publish_delivery.sh +269 -0
- package/evals/integration/test_reconcile_soundness.sh +528 -0
- package/evals/integration/test_resolvefirststep_security.sh +208 -0
- package/evals/integration/test_session_resume_roundtrip.sh +286 -0
- package/evals/integration/test_trust_checkpoint.sh +325 -0
- package/evals/integration/test_trust_reconcile.sh +293 -0
- package/evals/integration/test_verify_cli.sh +208 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
- package/evals/lib/node.sh +0 -6
- package/evals/run.sh +47 -0
- package/evals/static/test_workflow_skills.sh +6 -13
- package/install.sh +0 -7
- package/integrations/strands-ts/README.md +25 -15
- package/integrations/veritas/flow-agents.adapter.json +1 -2
- package/kits/builder/flows/build.flow.json +59 -12
- package/kits/builder/kit.json +85 -15
- package/kits/builder/skills/continue-work/SKILL.md +116 -0
- package/kits/builder/skills/deliver/SKILL.md +36 -6
- package/kits/builder/skills/design-probe/SKILL.md +28 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -1
- package/kits/builder/skills/gate-review/SKILL.md +234 -0
- package/kits/builder/skills/learning-review/SKILL.md +30 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
- package/kits/builder/skills/plan-work/SKILL.md +13 -1
- package/kits/builder/skills/pull-work/SKILL.md +19 -0
- package/kits/knowledge/adapters/default-store/index.js +38 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
- package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
- package/kits/knowledge/docs/store-contract.md +314 -0
- package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
- package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
- package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
- package/kits/knowledge/evals/entities/suite.test.js +40 -0
- package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
- package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
- package/kits/knowledge/evals/retirement/suite.test.js +145 -0
- package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
- package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
- package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
- package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
- package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
- package/kits/knowledge/kit.json +51 -1
- package/package.json +6 -6
- package/packaging/conformance/README.md +10 -2
- package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
- package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
- package/packaging/conformance/run-conformance.js +1 -1
- package/scripts/README.md +2 -1
- package/scripts/build-universal-bundles.js +0 -1
- package/scripts/ci/mint-attestation.js +221 -0
- package/scripts/ci/trust-reconcile.js +545 -0
- package/scripts/hooks/config-protection.js +423 -1
- package/scripts/hooks/evidence-capture.js +348 -0
- package/scripts/hooks/lib/liveness-read.js +113 -0
- package/scripts/hooks/run-hook.js +6 -1
- package/scripts/hooks/stop-goal-fit.js +1524 -79
- package/scripts/hooks/workflow-steering.js +135 -5
- package/scripts/install-codex-home.sh +39 -0
- package/scripts/install-merge.js +330 -0
- package/scripts/repair-command-log.js +115 -0
- package/src/cli/init.ts +218 -20
- package/src/cli/validate-workflow-artifacts.ts +18 -2
- package/src/cli/verify.ts +100 -0
- package/src/cli/workflow-sidecar.ts +2127 -84
- package/src/cli.ts +2 -3
- package/src/lib/flow-resolver.ts +369 -0
- package/src/tools/build-universal-bundles.ts +34 -21
- package/src/tools/generate-context-map.ts +3 -17
- package/src/tools/validate-source-tree.ts +44 -104
- package/build/src/tools/filter-installed-packs.d.ts +0 -2
- package/build/src/tools/filter-installed-packs.js +0 -135
- package/packaging/packs.json +0 -49
- package/scripts/filter-installed-packs.js +0 -2
- package/src/tools/filter-installed-packs.ts +0 -132
package/build/src/cli/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import * as os from "node:os";
|
|
5
6
|
import * as path from "node:path";
|
|
6
7
|
import { createInterface } from "node:readline/promises";
|
|
@@ -76,12 +77,19 @@ function usage() {
|
|
|
76
77
|
Options:
|
|
77
78
|
--runtime base|codex|claude-code|kiro|opencode|pi
|
|
78
79
|
--dest PATH
|
|
80
|
+
--global Target the runtime's global/user-level config path.
|
|
81
|
+
claude-code: merges FA hooks into ~/.claude/settings.json.
|
|
82
|
+
Honors FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation.
|
|
83
|
+
opencode: merges opencode.json into ~/.config/opencode/opencode.json
|
|
84
|
+
(honors XDG_CONFIG_HOME; test isolation via FLOW_AGENTS_USER_OPENCODE_CONFIG).
|
|
85
|
+
codex: runs install-codex-home.sh into ~/.flow-agents/codex
|
|
86
|
+
(the isolated Codex HOME; hooks merged, not overwritten).
|
|
87
|
+
pi: NOT_VERIFIED (no documented global dir); warns and falls back to workspace default.
|
|
79
88
|
--telemetry-sink local-files|local-kontour-console|kontour-hosted-console|user-hosted-console
|
|
80
89
|
--console-url URL
|
|
81
90
|
--console-endpoint URL
|
|
82
91
|
--console-token-file PATH
|
|
83
92
|
--console-tenant ID
|
|
84
|
-
--packs LIST
|
|
85
93
|
--activate-kits
|
|
86
94
|
--yes, --headless
|
|
87
95
|
`);
|
|
@@ -148,6 +156,35 @@ function defaultDest(runtime) {
|
|
|
148
156
|
return path.join(os.homedir(), ".flow-agents");
|
|
149
157
|
return process.cwd();
|
|
150
158
|
}
|
|
159
|
+
function globalDest(runtime) {
|
|
160
|
+
if (runtime === "claude-code") {
|
|
161
|
+
// Honor FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation (same as checkScopeCollision).
|
|
162
|
+
const override = process.env["FLOW_AGENTS_USER_CLAUDE_SETTINGS"];
|
|
163
|
+
if (override)
|
|
164
|
+
return path.dirname(override);
|
|
165
|
+
return path.join(os.homedir(), ".claude");
|
|
166
|
+
}
|
|
167
|
+
if (runtime === "opencode") {
|
|
168
|
+
// Honor FLOW_AGENTS_USER_OPENCODE_CONFIG (points to the opencode.json FILE) for test isolation,
|
|
169
|
+
// mirroring FLOW_AGENTS_USER_CLAUDE_SETTINGS for claude-code.
|
|
170
|
+
const override = process.env["FLOW_AGENTS_USER_OPENCODE_CONFIG"];
|
|
171
|
+
if (override)
|
|
172
|
+
return path.dirname(override);
|
|
173
|
+
// Global opencode config: ~/.config/opencode/ (honor XDG_CONFIG_HOME when set, else ~/.config).
|
|
174
|
+
return path.join(process.env["XDG_CONFIG_HOME"] ?? path.join(os.homedir(), ".config"), "opencode");
|
|
175
|
+
}
|
|
176
|
+
if (runtime === "codex") {
|
|
177
|
+
// codex --global routes to the isolated Codex HOME at ~/.flow-agents/codex.
|
|
178
|
+
// This is the same path used by install-codex-home.sh; --dest overrides it for sandbox testing.
|
|
179
|
+
return path.join(os.homedir(), ".flow-agents", "codex");
|
|
180
|
+
}
|
|
181
|
+
if (runtime === "pi") {
|
|
182
|
+
// pi has no documented global config dir.
|
|
183
|
+
// NOT_VERIFIED: fall back to workspace default and warn caller.
|
|
184
|
+
return defaultDest(runtime);
|
|
185
|
+
}
|
|
186
|
+
return defaultDest(runtime);
|
|
187
|
+
}
|
|
151
188
|
function parseYesNo(value, fallback) {
|
|
152
189
|
const normalized = value.trim().toLowerCase();
|
|
153
190
|
if (!normalized)
|
|
@@ -165,7 +202,9 @@ async function interactiveOptions(argv) {
|
|
|
165
202
|
const runtimeDefault = normalizeRuntime(flagString(args.flags, "runtime")) ?? "base";
|
|
166
203
|
const runtimeAnswer = flagString(args.flags, "runtime") ?? await rl.question(`Runtime [${runtimeDefault}]: `);
|
|
167
204
|
const runtime = normalizeRuntime(runtimeAnswer.trim() || runtimeDefault) ?? runtimeDefault;
|
|
168
|
-
const
|
|
205
|
+
const isGlobal = flagBool(args.flags, "global");
|
|
206
|
+
const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
|
|
207
|
+
const destDefault = flagString(args.flags, "dest", destBase) ?? destBase;
|
|
169
208
|
const destAnswer = flagString(args.flags, "dest") ?? await rl.question(`Install destination [${destDefault}]: `);
|
|
170
209
|
const sinkDefault = telemetrySinksFromFlags(args.flags);
|
|
171
210
|
const sinkAnswer = flagList(args.flags, "telemetry-sink").length || flagList(args.flags, "telemetry-sinks").length
|
|
@@ -186,6 +225,7 @@ async function interactiveOptions(argv) {
|
|
|
186
225
|
: "no";
|
|
187
226
|
return {
|
|
188
227
|
runtime,
|
|
228
|
+
global: isGlobal,
|
|
189
229
|
dest: path.resolve(destAnswer.trim() || destDefault),
|
|
190
230
|
consoleUrl: consoleUrl?.trim() || undefined,
|
|
191
231
|
consoleEndpoint: consoleEndpoint?.trim() || undefined,
|
|
@@ -193,7 +233,6 @@ async function interactiveOptions(argv) {
|
|
|
193
233
|
consoleTokenValue: consoleTokenValue?.trim() || undefined,
|
|
194
234
|
consoleTenant: consoleTenant?.trim() || undefined,
|
|
195
235
|
telemetrySinks,
|
|
196
|
-
packs: flagString(args.flags, "packs"),
|
|
197
236
|
activateKits: runtime === "codex" && parseYesNo(activateAnswer, activateDefault),
|
|
198
237
|
};
|
|
199
238
|
}
|
|
@@ -204,15 +243,17 @@ async function interactiveOptions(argv) {
|
|
|
204
243
|
function headlessOptions(argv) {
|
|
205
244
|
const args = parseArgs(argv);
|
|
206
245
|
const runtime = normalizeRuntime(flagString(args.flags, "runtime")) ?? "base";
|
|
246
|
+
const isGlobal = flagBool(args.flags, "global");
|
|
247
|
+
const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
|
|
207
248
|
return {
|
|
208
249
|
runtime,
|
|
209
|
-
|
|
250
|
+
global: isGlobal,
|
|
251
|
+
dest: path.resolve(flagString(args.flags, "dest", destBase) ?? destBase),
|
|
210
252
|
consoleUrl: flagString(args.flags, "console-url"),
|
|
211
253
|
consoleEndpoint: flagString(args.flags, "console-endpoint") ?? flagString(args.flags, "console-endpoint-url"),
|
|
212
254
|
consoleTokenFile: flagString(args.flags, "console-token-file"),
|
|
213
255
|
consoleTenant: flagString(args.flags, "console-tenant") ?? flagString(args.flags, "console-tenant-id"),
|
|
214
256
|
telemetrySinks: telemetrySinksFromFlags(args.flags),
|
|
215
|
-
packs: flagString(args.flags, "packs"),
|
|
216
257
|
activateKits: runtime === "codex" && flagBool(args.flags, "activate-kits"),
|
|
217
258
|
};
|
|
218
259
|
}
|
|
@@ -249,8 +290,6 @@ function installBundle(bundle, options) {
|
|
|
249
290
|
if (options.consoleTenant)
|
|
250
291
|
args.push("--console-tenant", options.consoleTenant);
|
|
251
292
|
const env = { ...process.env };
|
|
252
|
-
if (options.packs)
|
|
253
|
-
env.FLOW_AGENTS_PACKS = options.packs;
|
|
254
293
|
const result = spawnSync("bash", args, { cwd: bundle, env, encoding: "utf8", stdio: "inherit" });
|
|
255
294
|
if (tempTokenFile)
|
|
256
295
|
fs.rmSync(path.dirname(tempTokenFile), { recursive: true, force: true });
|
|
@@ -290,7 +329,136 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
290
329
|
// There is no well-known user-level codex hooks file in our install paths,
|
|
291
330
|
// so no collision check is needed for codex.
|
|
292
331
|
if (options.runtime === "claude-code") {
|
|
293
|
-
|
|
332
|
+
// Skip collision check when --global is set: the user is intentionally
|
|
333
|
+
// targeting the global settings, so the collision they'd create is expected.
|
|
334
|
+
if (!options.global)
|
|
335
|
+
checkScopeCollision();
|
|
336
|
+
}
|
|
337
|
+
// --global for claude-code: merge only into the global/user-level settings dir.
|
|
338
|
+
// This writes only the hook-wiring config (merge into ~/.claude/settings.json),
|
|
339
|
+
// not the full workspace bundle. The global settings dir is the claude config root,
|
|
340
|
+
// so the settings.json lives directly in dest (not dest/.claude/).
|
|
341
|
+
if (options.global && options.runtime === "claude-code") {
|
|
342
|
+
const bundle = ensureBundle(options.runtime);
|
|
343
|
+
// For --global, dest is ~/.claude/ (the global settings dir).
|
|
344
|
+
// dogfoodClaudeCode writes to dest/.claude/settings.json — but for global,
|
|
345
|
+
// the settings.json lives at dest/settings.json (dest IS ~/.claude/).
|
|
346
|
+
// We use a special global-merge path: merge directly into dest/settings.json.
|
|
347
|
+
const sourcePath = path.join(bundle, ".claude", "settings.json");
|
|
348
|
+
if (!fs.existsSync(sourcePath)) {
|
|
349
|
+
console.error(`flow-agents init: bundle settings missing: ${sourcePath}`);
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
353
|
+
// Remove permissive defaults (not appropriate for global user settings).
|
|
354
|
+
delete managed["permissions"];
|
|
355
|
+
delete managed["skipDangerousModePermissionPrompt"];
|
|
356
|
+
fs.mkdirSync(options.dest, { recursive: true });
|
|
357
|
+
const destSettingsPath = path.join(options.dest, "settings.json");
|
|
358
|
+
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
359
|
+
const _require = createRequire(import.meta.url);
|
|
360
|
+
const { mergeSettings } = _require(installMergePath);
|
|
361
|
+
let existing = {};
|
|
362
|
+
if (fs.existsSync(destSettingsPath)) {
|
|
363
|
+
try {
|
|
364
|
+
existing = JSON.parse(fs.readFileSync(destSettingsPath, "utf8"));
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
existing = {};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const merged = mergeSettings(existing, managed);
|
|
371
|
+
const tmp = `${destSettingsPath}.tmp.${process.pid}`;
|
|
372
|
+
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
373
|
+
fs.renameSync(tmp, destSettingsPath);
|
|
374
|
+
// Write version stamp.
|
|
375
|
+
const installRecordDir = path.join(options.dest, ".flow-agents");
|
|
376
|
+
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
377
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
378
|
+
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code", global: true };
|
|
379
|
+
const recordPath = path.join(installRecordDir, "install.json");
|
|
380
|
+
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
381
|
+
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
382
|
+
fs.renameSync(recordTmp, recordPath);
|
|
383
|
+
console.log(`Flow Agents global hooks merged for claude-code in ${options.dest}`);
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
// --global for opencode: merge FA opencode.json into the global opencode config dir.
|
|
387
|
+
// Global path: ~/.config/opencode/opencode.json (honor XDG_CONFIG_HOME).
|
|
388
|
+
// Test isolation: FLOW_AGENTS_USER_OPENCODE_CONFIG points to the opencode.json FILE.
|
|
389
|
+
if (options.global && options.runtime === "opencode") {
|
|
390
|
+
const bundle = ensureBundle(options.runtime);
|
|
391
|
+
const sourcePath = path.join(bundle, "opencode.json");
|
|
392
|
+
if (!fs.existsSync(sourcePath)) {
|
|
393
|
+
console.error(`flow-agents init: bundle opencode.json missing: ${sourcePath}`);
|
|
394
|
+
return 1;
|
|
395
|
+
}
|
|
396
|
+
const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
397
|
+
fs.mkdirSync(options.dest, { recursive: true });
|
|
398
|
+
// The global opencode.json lives directly at dest/opencode.json.
|
|
399
|
+
const destConfigPath = path.join(options.dest, "opencode.json");
|
|
400
|
+
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
401
|
+
const _require = createRequire(import.meta.url);
|
|
402
|
+
const { mergeSettings } = _require(installMergePath);
|
|
403
|
+
let existing = {};
|
|
404
|
+
if (fs.existsSync(destConfigPath)) {
|
|
405
|
+
try {
|
|
406
|
+
existing = JSON.parse(fs.readFileSync(destConfigPath, "utf8"));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
existing = {};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const merged = mergeSettings(existing, managed);
|
|
413
|
+
const tmp = `${destConfigPath}.tmp.${process.pid}`;
|
|
414
|
+
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}
|
|
415
|
+
`, "utf8");
|
|
416
|
+
fs.renameSync(tmp, destConfigPath);
|
|
417
|
+
// Write version stamp.
|
|
418
|
+
const installRecordDir = path.join(options.dest, ".flow-agents");
|
|
419
|
+
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
420
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
421
|
+
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "opencode", global: true };
|
|
422
|
+
const recordPath = path.join(installRecordDir, "install.json");
|
|
423
|
+
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
424
|
+
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}
|
|
425
|
+
`, "utf8");
|
|
426
|
+
fs.renameSync(recordTmp, recordPath);
|
|
427
|
+
console.log(`Flow Agents global config merged for opencode in ${options.dest}`);
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
// --global for codex: run install-codex-home.sh to install into the isolated Codex HOME.
|
|
431
|
+
// The codex --global path is ~/.flow-agents/codex (the isolated home IS codex's global).
|
|
432
|
+
// Pass through telemetry/console args and honor --dest override for sandbox testing.
|
|
433
|
+
if (options.global && options.runtime === "codex") {
|
|
434
|
+
const codexHomeScript = path.join(root, "scripts", "install-codex-home.sh");
|
|
435
|
+
if (!fs.existsSync(codexHomeScript)) {
|
|
436
|
+
console.error(`flow-agents init: install-codex-home.sh missing: ${codexHomeScript}`);
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
const scriptArgs = [options.dest];
|
|
440
|
+
for (const sink of options.telemetrySinks)
|
|
441
|
+
scriptArgs.push("--telemetry-sink", sink);
|
|
442
|
+
if (options.consoleUrl)
|
|
443
|
+
scriptArgs.push("--console-url", options.consoleUrl);
|
|
444
|
+
if (options.consoleEndpoint)
|
|
445
|
+
scriptArgs.push("--console-endpoint", options.consoleEndpoint);
|
|
446
|
+
if (options.consoleTokenFile)
|
|
447
|
+
scriptArgs.push("--console-token-file", options.consoleTokenFile);
|
|
448
|
+
if (options.consoleTenant)
|
|
449
|
+
scriptArgs.push("--console-tenant", options.consoleTenant);
|
|
450
|
+
const result = spawnSync("bash", [codexHomeScript, ...scriptArgs], { env: { ...process.env }, encoding: "utf8", stdio: "inherit" });
|
|
451
|
+
if (result.error) {
|
|
452
|
+
console.error(`flow-agents init: unable to run install-codex-home.sh: ${result.error.message}`);
|
|
453
|
+
return 1;
|
|
454
|
+
}
|
|
455
|
+
return result.status ?? 1;
|
|
456
|
+
}
|
|
457
|
+
// --global for pi: NOT_VERIFIED (no documented global dir). Warn and fall through to workspace install.
|
|
458
|
+
if (options.global && options.runtime === "pi") {
|
|
459
|
+
console.warn(`flow-agents init: NOT_VERIFIED: pi has no documented global config directory. ` +
|
|
460
|
+
`The --global flag for pi is not verified against pi documentation. ` +
|
|
461
|
+
`Falling back to workspace default destination: ${options.dest}`);
|
|
294
462
|
}
|
|
295
463
|
const bundle = ensureBundle(options.runtime);
|
|
296
464
|
const installed = installBundle(bundle, options);
|
|
@@ -337,39 +505,93 @@ function normalizeDogfoodRuntime(value) {
|
|
|
337
505
|
throw new Error(`dogfood: unsupported runtime '${value}'; expected claude-code, codex, opencode, or pi`);
|
|
338
506
|
}
|
|
339
507
|
/**
|
|
340
|
-
* Write the claude-code hook-wiring artifacts into dest.
|
|
508
|
+
* Write the claude-code hook-wiring artifacts into dest using merge semantics.
|
|
341
509
|
* Reads dist/claude-code/.claude/settings.json (generated by build-bundles),
|
|
342
|
-
* strips the permissive-mode permission keys (defaultMode, skipDangerousModePermissionPrompt)
|
|
343
|
-
*
|
|
510
|
+
* strips the permissive-mode permission keys (defaultMode, skipDangerousModePermissionPrompt)
|
|
511
|
+
* from the managed bundle settings, then MERGES into any existing dest settings.json —
|
|
512
|
+
* preserving user keys, auth, and non-flow-agents hooks. Writes version stamp.
|
|
344
513
|
*/
|
|
345
514
|
function dogfoodClaudeCode(bundleRoot, dest) {
|
|
346
515
|
const sourcePath = path.join(bundleRoot, ".claude", "settings.json");
|
|
347
516
|
if (!fs.existsSync(sourcePath))
|
|
348
517
|
throw new Error(`dogfood: bundle settings missing: ${sourcePath}`);
|
|
349
|
-
const
|
|
518
|
+
const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
350
519
|
// Remove permissive defaults that are only appropriate for installed workspaces.
|
|
351
520
|
// These keys must not be present in the source repo's .claude/settings.json.
|
|
352
|
-
delete
|
|
353
|
-
delete
|
|
521
|
+
delete managed["permissions"];
|
|
522
|
+
delete managed["skipDangerousModePermissionPrompt"];
|
|
354
523
|
const outDir = path.join(dest, ".claude");
|
|
355
524
|
fs.mkdirSync(outDir, { recursive: true });
|
|
356
|
-
|
|
525
|
+
const destSettingsPath = path.join(outDir, "settings.json");
|
|
526
|
+
// Merge: read existing, strip FA hooks, append new FA hooks, preserve all other keys.
|
|
527
|
+
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
528
|
+
const _require = createRequire(import.meta.url);
|
|
529
|
+
const { mergeSettings } = _require(installMergePath);
|
|
530
|
+
let existing = {};
|
|
531
|
+
if (fs.existsSync(destSettingsPath)) {
|
|
532
|
+
try {
|
|
533
|
+
existing = JSON.parse(fs.readFileSync(destSettingsPath, "utf8"));
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
existing = {};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const merged = mergeSettings(existing, managed);
|
|
540
|
+
const tmp = `${destSettingsPath}.tmp.${process.pid}`;
|
|
541
|
+
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
542
|
+
fs.renameSync(tmp, destSettingsPath);
|
|
543
|
+
// Write version stamp.
|
|
544
|
+
const installRecordDir = path.join(dest, ".flow-agents");
|
|
545
|
+
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
546
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
547
|
+
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code" };
|
|
548
|
+
const recordPath = path.join(installRecordDir, "install.json");
|
|
549
|
+
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
550
|
+
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
551
|
+
fs.renameSync(recordTmp, recordPath);
|
|
357
552
|
}
|
|
358
553
|
/**
|
|
359
|
-
* Write the codex hook-wiring artifacts into dest.
|
|
360
|
-
* Reads dist/codex/.codex/hooks.json and
|
|
554
|
+
* Write the codex hook-wiring artifacts into dest using merge semantics.
|
|
555
|
+
* Reads dist/codex/.codex/hooks.json and MERGES into any existing dest hooks.json —
|
|
556
|
+
* preserving user non-FA hook groups. Writes version stamp.
|
|
361
557
|
* The monolithic .codex/config.toml is not written here because it contains
|
|
362
558
|
* workspace settings (approvals_reviewer, features) that would override the
|
|
363
|
-
* developer's existing codex configuration. Only the hooks file is
|
|
559
|
+
* developer's existing codex configuration. Only the hooks file is merged.
|
|
364
560
|
*/
|
|
365
561
|
function dogfoodCodex(bundleRoot, dest) {
|
|
366
562
|
const sourcePath = path.join(bundleRoot, ".codex", "hooks.json");
|
|
367
563
|
if (!fs.existsSync(sourcePath))
|
|
368
564
|
throw new Error(`dogfood: bundle hooks.json missing: ${sourcePath}`);
|
|
369
|
-
const
|
|
565
|
+
const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
370
566
|
const outDir = path.join(dest, ".codex");
|
|
371
567
|
fs.mkdirSync(outDir, { recursive: true });
|
|
372
|
-
|
|
568
|
+
const destHooksPath = path.join(outDir, "hooks.json");
|
|
569
|
+
// Merge: read existing, strip FA hook-groups, append new FA hook-groups, preserve user groups.
|
|
570
|
+
const installMergePath = path.join(root, "scripts", "install-merge.js");
|
|
571
|
+
const _require = createRequire(import.meta.url);
|
|
572
|
+
const { mergeSettings } = _require(installMergePath);
|
|
573
|
+
let existing = {};
|
|
574
|
+
if (fs.existsSync(destHooksPath)) {
|
|
575
|
+
try {
|
|
576
|
+
existing = JSON.parse(fs.readFileSync(destHooksPath, "utf8"));
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
existing = {};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const merged = mergeSettings(existing, managed);
|
|
583
|
+
const tmp = `${destHooksPath}.tmp.${process.pid}`;
|
|
584
|
+
fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
585
|
+
fs.renameSync(tmp, destHooksPath);
|
|
586
|
+
// Write version stamp.
|
|
587
|
+
const installRecordDir = path.join(dest, ".flow-agents");
|
|
588
|
+
fs.mkdirSync(installRecordDir, { recursive: true });
|
|
589
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
590
|
+
const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "codex" };
|
|
591
|
+
const recordPath = path.join(installRecordDir, "install.json");
|
|
592
|
+
const recordTmp = `${recordPath}.tmp.${process.pid}`;
|
|
593
|
+
fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
594
|
+
fs.renameSync(recordTmp, recordPath);
|
|
373
595
|
}
|
|
374
596
|
/**
|
|
375
597
|
* Write the opencode hook-wiring artifacts into dest.
|
|
@@ -438,12 +438,29 @@ function validateSidecarGroup(inputs, markdown, requireSidecars, requireCritique
|
|
|
438
438
|
for (const dir of dirs) {
|
|
439
439
|
const deliver = markdown.find((p) => path.dirname(p) === dir && p.includes("deliver") && !p.includes("plan") && !p.includes("review"));
|
|
440
440
|
const delivered = deliver ? /status:\s*(delivered|accepted|archived)/i.test(readText(deliver)) : true;
|
|
441
|
-
|
|
441
|
+
// ADR 0010 Phase 4b: trust.bundle is the primary artifact at the delivered phase.
|
|
442
|
+
// evidence.json is OPTIONAL when trust.bundle is present (still schema-validated when present).
|
|
443
|
+
// Hard-fail on evidence.json absence only when no trust.bundle exists for a delivered session.
|
|
444
|
+
const hasTrustBundle = fs.existsSync(path.join(dir, "trust.bundle"));
|
|
445
|
+
const evidenceRequired = delivered && !hasTrustBundle;
|
|
446
|
+
for (const name of ["state.json", "acceptance.json", ...(evidenceRequired ? ["evidence.json"] : []), "handoff.json"]) {
|
|
442
447
|
if (!fs.existsSync(path.join(dir, name)))
|
|
443
448
|
issues.push({ path: path.join(dir, name), message: "required sidecar is missing" });
|
|
444
449
|
}
|
|
445
|
-
|
|
450
|
+
// ADR 0010 Phase 4c: critique.json no longer written; trust.bundle carries critique claims. Accept either.
|
|
451
|
+
if (requireCritique && !fs.existsSync(path.join(dir, "critique.json")) && !fs.existsSync(path.join(dir, "trust.bundle")))
|
|
446
452
|
issues.push({ path: path.join(dir, "critique.json"), message: "required sidecar is missing" });
|
|
453
|
+
// ADR 0010 Phase 4c: validate critique claims in trust.bundle (sole verification artifact).
|
|
454
|
+
const trustBundlePath = path.join(dir, "trust.bundle");
|
|
455
|
+
if (requireCritique && fs.existsSync(trustBundlePath)) {
|
|
456
|
+
const { value: bundleValue } = readJson(trustBundlePath);
|
|
457
|
+
if (bundleValue) {
|
|
458
|
+
const claims = Array.isArray(bundleValue.claims) ? bundleValue.claims : [];
|
|
459
|
+
const critiqueClaims = claims.filter((c) => c && c.claimType === "workflow.critique.review");
|
|
460
|
+
if (critiqueClaims.some((c) => c.value === "fail" || c.status === "disputed"))
|
|
461
|
+
issues.push({ path: trustBundlePath, message: "required critique must pass" });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
447
464
|
const acceptance = path.join(dir, "acceptance.json");
|
|
448
465
|
if (deliver && fs.existsSync(acceptance)) {
|
|
449
466
|
const expected = definitionAcceptanceCriteria(readText(deliver)).length;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseArgs, flagString, flagList } from "../lib/args.js";
|
|
6
|
+
import { root } from "../tools/common.js";
|
|
7
|
+
/**
|
|
8
|
+
* flow-agents verify — CI trust anchor for downstream repos.
|
|
9
|
+
*
|
|
10
|
+
* Re-runs canonical verification FRESH in the current environment, then reconciles
|
|
11
|
+
* a delivered trust.bundle's claimed passes against those fresh results. Exits 1
|
|
12
|
+
* on divergence, fresh-verify failure, laundered commands, or no verify configured.
|
|
13
|
+
* Exits 0 on clean pass.
|
|
14
|
+
*
|
|
15
|
+
* This is a thin wrapper around scripts/ci/trust-reconcile.js: it resolves the
|
|
16
|
+
* script from the installed package root (via createRequire) and calls the exported
|
|
17
|
+
* runTrustReconcile() function directly so all output goes to the same process
|
|
18
|
+
* stdout/stderr without an extra subprocess layer.
|
|
19
|
+
*
|
|
20
|
+
* The script ships in the npm package `files` list, so downstream repos always have
|
|
21
|
+
* access to it at the same relative path from the package root.
|
|
22
|
+
*/
|
|
23
|
+
const _require = createRequire(import.meta.url);
|
|
24
|
+
function usage() {
|
|
25
|
+
process.stderr.write("usage: flow-agents verify [--commands <cmd[,cmd...]>] [--bundle <path>] [--repo-root <path>]\n" +
|
|
26
|
+
"\n" +
|
|
27
|
+
"Re-runs canonical verification fresh and reconciles a delivered trust.bundle's\n" +
|
|
28
|
+
"claimed passes against CI results. Exits 1 on divergence, fresh-verify failure,\n" +
|
|
29
|
+
"compile-only/no-verify, or laundered commands. Exits 0 on clean pass.\n" +
|
|
30
|
+
"\n" +
|
|
31
|
+
"Options:\n" +
|
|
32
|
+
" --commands <cmd,...> Canonical verify command(s). Comma-separated or repeated.\n" +
|
|
33
|
+
" Falls back to TRUST_RECONCILE_COMMANDS env or\n" +
|
|
34
|
+
" package.json scripts['trust-reconcile-verify'].\n" +
|
|
35
|
+
" No-commands → fail-closed (compile-only refused).\n" +
|
|
36
|
+
" --bundle <path> Delivered trust.bundle or trust.checkpoint.json path.\n" +
|
|
37
|
+
" Falls back to TRUST_RECONCILE_BUNDLE env, then auto-\n" +
|
|
38
|
+
" discovers delivery/trust.bundle or delivery/trust.checkpoint.json.\n" +
|
|
39
|
+
" Absent bundle: only fresh verify is enforced (fail-open).\n" +
|
|
40
|
+
" --repo-root <path> Repository root. Default: TRUST_RECONCILE_REPO_ROOT env or cwd.\n" +
|
|
41
|
+
"\n" +
|
|
42
|
+
"Examples:\n" +
|
|
43
|
+
" # Re-run build+test fresh; reconcile against a delivered bundle:\n" +
|
|
44
|
+
" flow-agents verify --commands 'npm run build,npm test' --bundle delivery/trust.bundle\n" +
|
|
45
|
+
"\n" +
|
|
46
|
+
" # Fresh-verify only (no bundle), using package.json trust-reconcile-verify script:\n" +
|
|
47
|
+
" flow-agents verify\n");
|
|
48
|
+
}
|
|
49
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
50
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
51
|
+
usage();
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const { flags } = parseArgs(argv);
|
|
55
|
+
// --commands may be specified multiple times or comma-separated within a single value.
|
|
56
|
+
const commandsRaw = flagList(flags, "commands");
|
|
57
|
+
const commands = commandsRaw.flatMap((c) => c.split(",").map((s) => s.trim()).filter(Boolean));
|
|
58
|
+
const bundle = flagString(flags, "bundle") ?? null;
|
|
59
|
+
const repoRoot = flagString(flags, "repo-root") ?? null;
|
|
60
|
+
// Resolve the trust-reconcile.js script from the installed package root.
|
|
61
|
+
// `root` (from tools/common.ts) walks up from this compiled file's directory until
|
|
62
|
+
// it finds a directory with both package.json and packaging/ — the package root.
|
|
63
|
+
// In a downstream installed package: node_modules/@kontourai/flow-agents/
|
|
64
|
+
// In the dev repo: the repo root itself.
|
|
65
|
+
// scripts/ci/trust-reconcile.js ships in the npm package `files` list.
|
|
66
|
+
const reconcilePath = path.join(root, "scripts", "ci", "trust-reconcile.js");
|
|
67
|
+
if (!fs.existsSync(reconcilePath)) {
|
|
68
|
+
process.stderr.write(`[flow-agents verify] error: trust-reconcile.js not found at ${reconcilePath}\n` +
|
|
69
|
+
"[flow-agents verify] Is the package correctly installed? Expected scripts/ci/trust-reconcile.js.\n");
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
const { runTrustReconcile } = _require(reconcilePath);
|
|
73
|
+
return runTrustReconcile({ bundle, commands, repoRoot });
|
|
74
|
+
}
|
|
75
|
+
// Direct-script guard (mirrors pattern from other CLI subcommands).
|
|
76
|
+
const _selfReal = (() => { try {
|
|
77
|
+
return fs.realpathSync(fileURLToPath(import.meta.url));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return fileURLToPath(import.meta.url);
|
|
81
|
+
} })();
|
|
82
|
+
const _argv1Real = (() => { try {
|
|
83
|
+
return fs.realpathSync(process.argv[1]);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return process.argv[1];
|
|
87
|
+
} })();
|
|
88
|
+
if (_selfReal === _argv1Real) {
|
|
89
|
+
process.exitCode = await main();
|
|
90
|
+
}
|