@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.js
CHANGED
|
@@ -14,18 +14,17 @@ import { main as veritasGovernance } from "./cli/veritas-governance.js";
|
|
|
14
14
|
import { main as workflowArtifactCleanupAudit } from "./cli/workflow-artifact-cleanup-audit.js";
|
|
15
15
|
import { main as buildBundles } from "./tools/build-universal-bundles.js";
|
|
16
16
|
import { main as contextMap } from "./tools/generate-context-map.js";
|
|
17
|
-
import { main as filterInstalledPacks } from "./tools/filter-installed-packs.js";
|
|
18
17
|
import { main as validateSource } from "./tools/validate-source-tree.js";
|
|
19
18
|
import { main as validatePackage } from "./tools/validate-package.js";
|
|
20
19
|
import { main as validateHookInfluence } from "./cli/validate-hook-influence.js";
|
|
21
20
|
import { main as runtimeAdapter } from "./cli/runtime-adapter.js";
|
|
22
21
|
import { main as utteranceCheck } from "./cli/utterance-check.js";
|
|
22
|
+
import { main as verify } from "./cli/verify.js";
|
|
23
23
|
const availableCommands = new Map([
|
|
24
24
|
["build-bundles", () => buildBundles()],
|
|
25
25
|
["console-learning-projection", consoleLearningProjection],
|
|
26
26
|
["context-map", contextMap],
|
|
27
27
|
["effective-backlog-settings", effectiveBacklogSettings],
|
|
28
|
-
["filter-installed-packs", filterInstalledPacks],
|
|
29
28
|
["fixture-retirement-audit", fixtureRetirementAudit],
|
|
30
29
|
["kit", kit],
|
|
31
30
|
["init", init],
|
|
@@ -39,6 +38,7 @@ const availableCommands = new Map([
|
|
|
39
38
|
["veritas-governance", veritasGovernance],
|
|
40
39
|
["validate-package", validatePackage],
|
|
41
40
|
["validate-hook-influence", validateHookInfluence],
|
|
41
|
+
["verify", verify],
|
|
42
42
|
["validate-source", validateSource],
|
|
43
43
|
["workflow-artifact-cleanup-audit", workflowArtifactCleanupAudit],
|
|
44
44
|
]);
|
|
@@ -47,7 +47,6 @@ const aliases = new Map([
|
|
|
47
47
|
["flow-agents-console-learning-projection", "console-learning-projection"],
|
|
48
48
|
["flow-agents-context-map", "context-map"],
|
|
49
49
|
["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
|
|
50
|
-
["flow-agents-filter-installed-packs", "filter-installed-packs"],
|
|
51
50
|
["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
|
|
52
51
|
["flow-agents-kit", "kit"],
|
|
53
52
|
["flow-agents-promote-workflow-artifact", "promote-workflow-artifact"],
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow-Definition resolver — ADR 0016 Abstraction A (Option B), Phase P-a.
|
|
3
|
+
*
|
|
4
|
+
* Shared resolver consumed by both the producer (workflow-sidecar.ts) and,
|
|
5
|
+
* later, the enforcer (stop-goal-fit.js via P-c). This is the SINGLE source
|
|
6
|
+
* of truth for (active_flow_id, active_step_id) → FlowDefinition gate
|
|
7
|
+
* expects[] resolution. Neither consumer duplicates this logic.
|
|
8
|
+
*
|
|
9
|
+
* Design:
|
|
10
|
+
* - Pure and synchronous — no async, no throws.
|
|
11
|
+
* - Returns null on ENOENT, parse error, or missing gate (fail-open).
|
|
12
|
+
* - Kit-agnostic: kitId = flowId.split(".")[0]; no hardcoded kit list.
|
|
13
|
+
* - Honors FLOW_AGENTS_FLOW_DEFS_DIR env-var override for custom installs.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Build and validate the FlowDefinition file path.
|
|
17
|
+
*
|
|
18
|
+
* Returns the validated absolute file path, or null when:
|
|
19
|
+
* - kitId or flowName contains chars outside SLUG_RE (rejects traversal)
|
|
20
|
+
* - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a .flow-agents directory
|
|
21
|
+
* - The resolved path escapes the expected root (belt-and-suspenders)
|
|
22
|
+
*
|
|
23
|
+
* When the override is unsafe (points into .flow-agents), falls back silently
|
|
24
|
+
* to the repoRoot/kits/ path so the resolver still works for legit flows.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveFlowFilePath(kitId: string, flowName: string, flowId: string, repoRoot: string): string | null;
|
|
27
|
+
/** A single gate expectation from a FlowDefinition expects[] entry. */
|
|
28
|
+
export type GateExpectation = {
|
|
29
|
+
id: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
required: boolean;
|
|
32
|
+
bundle_claim: {
|
|
33
|
+
claimType: string;
|
|
34
|
+
subjectType: string;
|
|
35
|
+
accepted_statuses: string[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** The resolved result from the active flow step. */
|
|
39
|
+
export type ActiveFlowStep = {
|
|
40
|
+
flowId: string;
|
|
41
|
+
stepId: string;
|
|
42
|
+
gateId: string;
|
|
43
|
+
gateExpects: GateExpectation[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the gate expects[] for a specific (flowId, stepId) pair.
|
|
47
|
+
*
|
|
48
|
+
* @param flowId e.g. "builder.build" — kitId is extracted as the prefix before the first ".".
|
|
49
|
+
* @param stepId e.g. "verify" — matched against gate.step values in the FlowDefinition.
|
|
50
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
51
|
+
* Honored only when FLOW_AGENTS_FLOW_DEFS_DIR is not set.
|
|
52
|
+
* @returns ActiveFlowStep with the matched gate's expects[], or null on any error.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveFlowStep(flowId: string, stepId: string, repoRoot: string): ActiveFlowStep | null;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the phase→step mapping from a FlowDefinition's phase_map field.
|
|
57
|
+
*
|
|
58
|
+
* Returns the phase_map object (e.g. {"execution":"execute","planning":"plan",...})
|
|
59
|
+
* or null when the flow file cannot be loaded, the phase_map field is absent, or
|
|
60
|
+
* the field is not a plain Record<string,string>.
|
|
61
|
+
*
|
|
62
|
+
* Pure and synchronous — no throws, fail-open on any error.
|
|
63
|
+
*
|
|
64
|
+
* @param flowId e.g. "builder.build" — kitId is the prefix before the first ".".
|
|
65
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
66
|
+
* Honored only when FLOW_AGENTS_FLOW_DEFS_DIR is not set.
|
|
67
|
+
* @returns Record<string,string> phase→stepId map, or null on absence/error.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolvePhaseMap(flowId: string, repoRoot: string): Record<string, string> | null;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the active flow step from current.json.
|
|
72
|
+
*
|
|
73
|
+
* Reads active_flow_id and active_step_id from <flowAgentsDir>/current.json.
|
|
74
|
+
* If both are present, delegates to resolveFlowStep. The repoRoot is derived by
|
|
75
|
+
* walking upward from flowAgentsDir to find the nearest ancestor containing kits/,
|
|
76
|
+
* with a fallback to process.cwd(). This handles temp dirs, CI workspaces, and
|
|
77
|
+
* subproject layouts without hardcoding the repo structure.
|
|
78
|
+
*
|
|
79
|
+
* @param flowAgentsDir Path to the .flow-agents directory (contains current.json).
|
|
80
|
+
* @returns ActiveFlowStep or null when fields are absent or resolution fails.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveActiveFlowStep(flowAgentsDir: string): ActiveFlowStep | null;
|
|
83
|
+
/** The resolved route-back policy for a phase transition. */
|
|
84
|
+
export type RouteBackPolicy = {
|
|
85
|
+
/** Maximum allowed route-back attempts for this transition key. */
|
|
86
|
+
maxAttempts: number;
|
|
87
|
+
/** Action when attempts are exceeded (e.g. "block"). */
|
|
88
|
+
onExceeded: string;
|
|
89
|
+
/** The step id whose gate declared this policy (e.g. "verify"). */
|
|
90
|
+
fromStepId: string;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the route-back policy for a phase transition, if the active FlowDefinition
|
|
94
|
+
* declares one on the source phase's gate.
|
|
95
|
+
*
|
|
96
|
+
* A route-back is a transition where the source phase's gate declares both
|
|
97
|
+
* `route_back_policy` and `on_route_back`, and the target phase maps to a step
|
|
98
|
+
* listed as a route-back target in `on_route_back` values.
|
|
99
|
+
*
|
|
100
|
+
* This is the FlowDefinition-driven replacement for the hardcoded
|
|
101
|
+
* `flow === "builder.build" && prev.phase === "verification" && phase === "execution"`
|
|
102
|
+
* guard in advance-state. Any flow that declares `route_back_policy` on a gate
|
|
103
|
+
* automatically gets route-back enforcement without code changes.
|
|
104
|
+
*
|
|
105
|
+
* @param flowId e.g. "builder.build" — kitId is the prefix before the first ".".
|
|
106
|
+
* @param fromPhase Lifecycle phase leaving (e.g. "verification").
|
|
107
|
+
* @param toPhase Lifecycle phase entering (e.g. "execution").
|
|
108
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
109
|
+
* @returns RouteBackPolicy when the transition is a declared route-back, null otherwise.
|
|
110
|
+
*/
|
|
111
|
+
export declare function resolveRouteBackPolicy(flowId: string, fromPhase: string, toPhase: string, repoRoot: string): RouteBackPolicy | null;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow-Definition resolver — ADR 0016 Abstraction A (Option B), Phase P-a.
|
|
3
|
+
*
|
|
4
|
+
* Shared resolver consumed by both the producer (workflow-sidecar.ts) and,
|
|
5
|
+
* later, the enforcer (stop-goal-fit.js via P-c). This is the SINGLE source
|
|
6
|
+
* of truth for (active_flow_id, active_step_id) → FlowDefinition gate
|
|
7
|
+
* expects[] resolution. Neither consumer duplicates this logic.
|
|
8
|
+
*
|
|
9
|
+
* Design:
|
|
10
|
+
* - Pure and synchronous — no async, no throws.
|
|
11
|
+
* - Returns null on ENOENT, parse error, or missing gate (fail-open).
|
|
12
|
+
* - Kit-agnostic: kitId = flowId.split(".")[0]; no hardcoded kit list.
|
|
13
|
+
* - Honors FLOW_AGENTS_FLOW_DEFS_DIR env-var override for custom installs.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
// ─── Security: Layer 1 traversal defense ─────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// Both kitId and flowName originate from agent-writable sources (active_flow_id
|
|
20
|
+
// in current.json, and FLOW_AGENTS_FLOW_DEFS_DIR set by the runtime). A crafted
|
|
21
|
+
// value like "builder.../../../.flow-agents/slug/fake-flow" produces:
|
|
22
|
+
// kitId = "builder"
|
|
23
|
+
// flowName = "../../../.flow-agents/slug/fake-flow"
|
|
24
|
+
// which resolves OUTSIDE kits/ via path.join traversal.
|
|
25
|
+
//
|
|
26
|
+
// SLUG_RE closes this: it rejects any value containing path separators, dots,
|
|
27
|
+
// or characters outside the safe identifier alphabet. Only [a-zA-Z0-9_-] is
|
|
28
|
+
// allowed, making traversal sequences impossible.
|
|
29
|
+
//
|
|
30
|
+
// Belt-and-suspenders: after building the path we also confirm the resolved
|
|
31
|
+
// absolute path stays inside the expected root directory.
|
|
32
|
+
/** Strict slug pattern — allows only URL-safe identifier chars. */
|
|
33
|
+
const SLUG_RE = /^[a-zA-Z0-9_-]+$/;
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when the given resolved absolute path falls within a .flow-agents
|
|
36
|
+
* directory (an agent-writable area). Used to reject FLOW_AGENTS_FLOW_DEFS_DIR
|
|
37
|
+
* overrides that point into agent-controlled storage.
|
|
38
|
+
*/
|
|
39
|
+
function isAgentWritableDir(resolvedDir) {
|
|
40
|
+
return resolvedDir.split(path.sep).includes(".flow-agents");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build and validate the FlowDefinition file path.
|
|
44
|
+
*
|
|
45
|
+
* Returns the validated absolute file path, or null when:
|
|
46
|
+
* - kitId or flowName contains chars outside SLUG_RE (rejects traversal)
|
|
47
|
+
* - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a .flow-agents directory
|
|
48
|
+
* - The resolved path escapes the expected root (belt-and-suspenders)
|
|
49
|
+
*
|
|
50
|
+
* When the override is unsafe (points into .flow-agents), falls back silently
|
|
51
|
+
* to the repoRoot/kits/ path so the resolver still works for legit flows.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveFlowFilePath(kitId, flowName, flowId, repoRoot) {
|
|
54
|
+
// Primary defense: reject any slug containing traversal chars or non-identifier chars.
|
|
55
|
+
if (!SLUG_RE.test(kitId) || !SLUG_RE.test(flowName))
|
|
56
|
+
return null;
|
|
57
|
+
const override = process.env["FLOW_AGENTS_FLOW_DEFS_DIR"];
|
|
58
|
+
let expectedRoot;
|
|
59
|
+
let flowFilePath;
|
|
60
|
+
if (override) {
|
|
61
|
+
const resolvedOverride = path.resolve(override);
|
|
62
|
+
if (isAgentWritableDir(resolvedOverride)) {
|
|
63
|
+
// Override targets an agent-writable .flow-agents path — reject it and
|
|
64
|
+
// fall back to the kit root. The session will resolve the real kit flow.
|
|
65
|
+
expectedRoot = path.resolve(repoRoot, "kits");
|
|
66
|
+
flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
expectedRoot = resolvedOverride;
|
|
70
|
+
// flowId = kitId + "." + flowName; after slug validation this contains only
|
|
71
|
+
// [a-zA-Z0-9_-.] — no slashes, no traversal.
|
|
72
|
+
flowFilePath = path.join(resolvedOverride, `${flowId}.flow.json`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
expectedRoot = path.resolve(repoRoot, "kits");
|
|
77
|
+
flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
|
|
78
|
+
}
|
|
79
|
+
// Belt-and-suspenders: confirm the resolved path stays within the expected root.
|
|
80
|
+
// After slug validation this is theoretically unreachable, but defense-in-depth
|
|
81
|
+
// verifies the invariant rather than merely asserting it.
|
|
82
|
+
const resolvedPath = path.resolve(flowFilePath);
|
|
83
|
+
if (!resolvedPath.startsWith(expectedRoot + path.sep) && resolvedPath !== expectedRoot) {
|
|
84
|
+
return null; // traversal still detected — paranoid fallback
|
|
85
|
+
}
|
|
86
|
+
return resolvedPath;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the gate expects[] for a specific (flowId, stepId) pair.
|
|
90
|
+
*
|
|
91
|
+
* @param flowId e.g. "builder.build" — kitId is extracted as the prefix before the first ".".
|
|
92
|
+
* @param stepId e.g. "verify" — matched against gate.step values in the FlowDefinition.
|
|
93
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
94
|
+
* Honored only when FLOW_AGENTS_FLOW_DEFS_DIR is not set.
|
|
95
|
+
* @returns ActiveFlowStep with the matched gate's expects[], or null on any error.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveFlowStep(flowId, stepId, repoRoot) {
|
|
98
|
+
if (!flowId || !stepId)
|
|
99
|
+
return null;
|
|
100
|
+
const dotIdx = flowId.indexOf(".");
|
|
101
|
+
if (dotIdx < 1)
|
|
102
|
+
return null; // flowId must have at least one "." to derive kitId
|
|
103
|
+
const kitId = flowId.slice(0, dotIdx);
|
|
104
|
+
// The flow filename is the part after the first "." (e.g. "build" from "builder.build")
|
|
105
|
+
const flowName = flowId.slice(dotIdx + 1);
|
|
106
|
+
if (!kitId || !flowName)
|
|
107
|
+
return null;
|
|
108
|
+
// Layer 1 defense: validate stepId too — it is matched against gate.step values but
|
|
109
|
+
// still originates from agent-writable current.json active_step_id.
|
|
110
|
+
if (!SLUG_RE.test(stepId))
|
|
111
|
+
return null;
|
|
112
|
+
// Determine the FlowDefinition file path with slug validation + path containment check.
|
|
113
|
+
// Returns null for traversal attempts (e.g. flowName = "../../../.flow-agents/fake").
|
|
114
|
+
const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
|
|
115
|
+
if (!flowFilePath)
|
|
116
|
+
return null;
|
|
117
|
+
let flowDef;
|
|
118
|
+
try {
|
|
119
|
+
const raw = fs.readFileSync(flowFilePath, "utf8");
|
|
120
|
+
flowDef = JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null; // ENOENT, permission error, or parse error → fail-open
|
|
124
|
+
}
|
|
125
|
+
if (!flowDef || typeof flowDef !== "object" || !flowDef.gates)
|
|
126
|
+
return null;
|
|
127
|
+
// Find the gate whose .step matches stepId.
|
|
128
|
+
for (const [gateId, gate] of Object.entries(flowDef.gates)) {
|
|
129
|
+
if (!gate || gate.step !== stepId)
|
|
130
|
+
continue;
|
|
131
|
+
const expects = Array.isArray(gate.expects) ? gate.expects : [];
|
|
132
|
+
return { flowId, stepId, gateId, gateExpects: expects };
|
|
133
|
+
}
|
|
134
|
+
return null; // no gate matched the given stepId
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the phase→step mapping from a FlowDefinition's phase_map field.
|
|
138
|
+
*
|
|
139
|
+
* Returns the phase_map object (e.g. {"execution":"execute","planning":"plan",...})
|
|
140
|
+
* or null when the flow file cannot be loaded, the phase_map field is absent, or
|
|
141
|
+
* the field is not a plain Record<string,string>.
|
|
142
|
+
*
|
|
143
|
+
* Pure and synchronous — no throws, fail-open on any error.
|
|
144
|
+
*
|
|
145
|
+
* @param flowId e.g. "builder.build" — kitId is the prefix before the first ".".
|
|
146
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
147
|
+
* Honored only when FLOW_AGENTS_FLOW_DEFS_DIR is not set.
|
|
148
|
+
* @returns Record<string,string> phase→stepId map, or null on absence/error.
|
|
149
|
+
*/
|
|
150
|
+
export function resolvePhaseMap(flowId, repoRoot) {
|
|
151
|
+
if (!flowId)
|
|
152
|
+
return null;
|
|
153
|
+
const dotIdx = flowId.indexOf(".");
|
|
154
|
+
if (dotIdx < 1)
|
|
155
|
+
return null;
|
|
156
|
+
const kitId = flowId.slice(0, dotIdx);
|
|
157
|
+
const flowName = flowId.slice(dotIdx + 1);
|
|
158
|
+
if (!kitId || !flowName)
|
|
159
|
+
return null;
|
|
160
|
+
// Layer 1 defense: same slug validation + path containment as resolveFlowStep.
|
|
161
|
+
const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
|
|
162
|
+
if (!flowFilePath)
|
|
163
|
+
return null;
|
|
164
|
+
let flowDef;
|
|
165
|
+
try {
|
|
166
|
+
const raw = fs.readFileSync(flowFilePath, "utf8");
|
|
167
|
+
flowDef = JSON.parse(raw);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null; // ENOENT, permission error, or parse error → fail-open
|
|
171
|
+
}
|
|
172
|
+
if (!flowDef || typeof flowDef !== "object")
|
|
173
|
+
return null;
|
|
174
|
+
const pm = flowDef.phase_map;
|
|
175
|
+
if (!pm || typeof pm !== "object" || Array.isArray(pm))
|
|
176
|
+
return null;
|
|
177
|
+
// Validate: all values must be strings
|
|
178
|
+
for (const v of Object.values(pm)) {
|
|
179
|
+
if (typeof v !== "string")
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return pm;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Find the repository root from a starting directory by walking upward to locate
|
|
186
|
+
* the nearest ancestor that contains a `kits/` subdirectory. If none is found,
|
|
187
|
+
* falls back to `process.cwd()` so the default "run from repo root" case still works.
|
|
188
|
+
*
|
|
189
|
+
* This is required because the .flow-agents directory can live anywhere (temp dirs,
|
|
190
|
+
* subprojects, CI workspaces) while the kits/ directory is always at the repo root.
|
|
191
|
+
*/
|
|
192
|
+
function findRepoRoot(startDir) {
|
|
193
|
+
// Walk up from startDir looking for a kits/ directory
|
|
194
|
+
let dir = startDir;
|
|
195
|
+
for (let i = 0; i < 16; i++) {
|
|
196
|
+
if (fs.existsSync(path.join(dir, "kits")))
|
|
197
|
+
return dir;
|
|
198
|
+
const parent = path.dirname(dir);
|
|
199
|
+
if (parent === dir)
|
|
200
|
+
break; // reached filesystem root
|
|
201
|
+
dir = parent;
|
|
202
|
+
}
|
|
203
|
+
// Fallback: process.cwd() covers the common "run from repo root" case
|
|
204
|
+
return process.cwd();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Resolve the active flow step from current.json.
|
|
208
|
+
*
|
|
209
|
+
* Reads active_flow_id and active_step_id from <flowAgentsDir>/current.json.
|
|
210
|
+
* If both are present, delegates to resolveFlowStep. The repoRoot is derived by
|
|
211
|
+
* walking upward from flowAgentsDir to find the nearest ancestor containing kits/,
|
|
212
|
+
* with a fallback to process.cwd(). This handles temp dirs, CI workspaces, and
|
|
213
|
+
* subproject layouts without hardcoding the repo structure.
|
|
214
|
+
*
|
|
215
|
+
* @param flowAgentsDir Path to the .flow-agents directory (contains current.json).
|
|
216
|
+
* @returns ActiveFlowStep or null when fields are absent or resolution fails.
|
|
217
|
+
*/
|
|
218
|
+
export function resolveActiveFlowStep(flowAgentsDir) {
|
|
219
|
+
if (!flowAgentsDir)
|
|
220
|
+
return null;
|
|
221
|
+
const currentFile = path.join(flowAgentsDir, "current.json");
|
|
222
|
+
let current;
|
|
223
|
+
try {
|
|
224
|
+
const raw = fs.readFileSync(currentFile, "utf8");
|
|
225
|
+
current = JSON.parse(raw);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const flowId = typeof current["active_flow_id"] === "string" ? current["active_flow_id"] : null;
|
|
231
|
+
const stepId = typeof current["active_step_id"] === "string" ? current["active_step_id"] : null;
|
|
232
|
+
if (!flowId || !stepId)
|
|
233
|
+
return null;
|
|
234
|
+
// Find repoRoot: walk up from flowAgentsDir to find kits/, fallback to cwd
|
|
235
|
+
const repoRoot = findRepoRoot(path.dirname(flowAgentsDir));
|
|
236
|
+
return resolveFlowStep(flowId, stepId, repoRoot);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the route-back policy for a phase transition, if the active FlowDefinition
|
|
240
|
+
* declares one on the source phase's gate.
|
|
241
|
+
*
|
|
242
|
+
* A route-back is a transition where the source phase's gate declares both
|
|
243
|
+
* `route_back_policy` and `on_route_back`, and the target phase maps to a step
|
|
244
|
+
* listed as a route-back target in `on_route_back` values.
|
|
245
|
+
*
|
|
246
|
+
* This is the FlowDefinition-driven replacement for the hardcoded
|
|
247
|
+
* `flow === "builder.build" && prev.phase === "verification" && phase === "execution"`
|
|
248
|
+
* guard in advance-state. Any flow that declares `route_back_policy` on a gate
|
|
249
|
+
* automatically gets route-back enforcement without code changes.
|
|
250
|
+
*
|
|
251
|
+
* @param flowId e.g. "builder.build" — kitId is the prefix before the first ".".
|
|
252
|
+
* @param fromPhase Lifecycle phase leaving (e.g. "verification").
|
|
253
|
+
* @param toPhase Lifecycle phase entering (e.g. "execution").
|
|
254
|
+
* @param repoRoot Absolute path to the repository root (kits/ lives here).
|
|
255
|
+
* @returns RouteBackPolicy when the transition is a declared route-back, null otherwise.
|
|
256
|
+
*/
|
|
257
|
+
export function resolveRouteBackPolicy(flowId, fromPhase, toPhase, repoRoot) {
|
|
258
|
+
if (!flowId || !fromPhase || !toPhase)
|
|
259
|
+
return null;
|
|
260
|
+
const dotIdx = flowId.indexOf(".");
|
|
261
|
+
if (dotIdx < 1)
|
|
262
|
+
return null;
|
|
263
|
+
const kitId = flowId.slice(0, dotIdx);
|
|
264
|
+
const flowName = flowId.slice(dotIdx + 1);
|
|
265
|
+
if (!kitId || !flowName)
|
|
266
|
+
return null;
|
|
267
|
+
const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
|
|
268
|
+
if (!flowFilePath)
|
|
269
|
+
return null;
|
|
270
|
+
let flowDef;
|
|
271
|
+
try {
|
|
272
|
+
const raw = fs.readFileSync(flowFilePath, "utf8");
|
|
273
|
+
flowDef = JSON.parse(raw);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null; // ENOENT, permission error, or parse error — fail-open
|
|
277
|
+
}
|
|
278
|
+
if (!flowDef || typeof flowDef !== "object")
|
|
279
|
+
return null;
|
|
280
|
+
const phaseMap = flowDef.phase_map;
|
|
281
|
+
if (!phaseMap || typeof phaseMap !== "object" || Array.isArray(phaseMap))
|
|
282
|
+
return null;
|
|
283
|
+
const fromStep = phaseMap[fromPhase];
|
|
284
|
+
const toStep = phaseMap[toPhase];
|
|
285
|
+
if (!fromStep || !toStep)
|
|
286
|
+
return null; // phases not in this flow
|
|
287
|
+
if (!flowDef.gates)
|
|
288
|
+
return null;
|
|
289
|
+
for (const gate of Object.values(flowDef.gates)) {
|
|
290
|
+
if (!gate || gate.step !== fromStep)
|
|
291
|
+
continue;
|
|
292
|
+
if (!gate.route_back_policy || !gate.on_route_back)
|
|
293
|
+
return null;
|
|
294
|
+
// Check if toStep is a valid route-back target declared in on_route_back
|
|
295
|
+
const routeBackTargets = Object.values(gate.on_route_back);
|
|
296
|
+
if (!routeBackTargets.includes(toStep))
|
|
297
|
+
return null;
|
|
298
|
+
const maxAttempts = typeof gate.route_back_policy.max_attempts === "number"
|
|
299
|
+
? gate.route_back_policy.max_attempts
|
|
300
|
+
: 3;
|
|
301
|
+
return {
|
|
302
|
+
maxAttempts,
|
|
303
|
+
onExceeded: gate.route_back_policy.on_exceeded ?? "block",
|
|
304
|
+
fromStepId: fromStep,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { loadJson, readText, root, walkFiles, writeText } from "./common.js";
|
|
6
6
|
const dist = process.env.FLOW_AGENTS_DIST_DIR ? path.resolve(process.env.FLOW_AGENTS_DIST_DIR) : path.join(root, "dist");
|
|
7
7
|
const manifest = loadJson(path.join(root, "packaging/manifest.json"));
|
|
8
|
-
const
|
|
8
|
+
const pkgVersion = loadJson(path.join(root, "package.json"))["version"] ?? "0.0.0";
|
|
9
9
|
const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".sh", ".toml", ".txt", ".yaml", ".yml", ".ts"]);
|
|
10
10
|
const dropDiagnostics = [];
|
|
11
11
|
const printDiagnostics = !["0", "false", "no"].includes(String(process.env.FLOW_AGENTS_EXPORT_DIAGNOSTICS ?? "1").toLowerCase());
|
|
@@ -190,8 +190,9 @@ function generatedAgentsSummary(agents) {
|
|
|
190
190
|
function exportRootAgentsMd(label, agents, taskDir) {
|
|
191
191
|
return `# Universal Agent Bundle (${label})\n\nThis bundle was generated from the canonical source in this repo. Treat the repo root as the source of truth and regenerate the bundle instead of editing exported agent files by hand.\n\n## Shared Conventions\n\n- \`skills/\`, \`context/\`, \`powers/\`, \`prompts/\`, \`scripts/\`, and \`evals/\` were copied from the canonical source.\n- Cross-session task artifacts should live under \`${taskDir}\`.\n- Kiro-only hook wiring was stripped from exported non-Kiro agents to keep the package portable.\n\n## Exported Agents\n\n${generatedAgentsSummary(agents)}\n`;
|
|
192
192
|
}
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
const CODEX_LIVE_HOOKS_README = `\n## Running with hooks active (live)\n\n\`install.sh\` lays the bundle into a workspace, but a live Codex session needs a \`CODEX_HOME\` that has both the bundle's hooks/scripts AND your real credentials. Use the dedicated installer, which flattens the config to the home root and copies your auth from \`~/.codex\`:\n\n\`\`\`bash\nbash scripts/install-codex-home.sh "$HOME/.flow-agents/codex"\nCODEX_HOME="$HOME/.flow-agents/codex" codex exec --dangerously-bypass-hook-trust -C /path/to/project "<prompt>"\n\`\`\`\n\nThe goal-fit Stop hook then enforces by default (\`FLOW_AGENTS_GOAL_FIT_MODE=block\`); set it to \`warn\` or \`off\` to override.\n`;
|
|
194
|
+
function exportTargetReadme(label, installHint, extra = "") {
|
|
195
|
+
return `# ${label} Bundle\n\nGenerated from the canonical source in this repository.\n\n## Install\n\n\`\`\`bash\n${installHint}\n\`\`\`\n\nThe install ships the full standalone base (skills, agents, powers) plus the\nFlow Kits. Kit depth is activated through the Kit Catalog, not at install time.\n\n## Contents\n\n- Harness-specific agents\n- Shared skills\n- Shared context, powers, prompts, scripts, and evals\n${extra}`;
|
|
195
196
|
}
|
|
196
197
|
function mapClaudeTools(allowedTools) {
|
|
197
198
|
const ordered = [];
|
|
@@ -277,8 +278,8 @@ function shellHook(command, timeout = 10, statusMessage) {
|
|
|
277
278
|
function claudeTelemetry(event) {
|
|
278
279
|
return `bash -lc 'root="\${CLAUDE_PROJECT_DIR:-$(pwd)}"; node "$root/scripts/hooks/claude-telemetry-hook.js" ${event} dev'`;
|
|
279
280
|
}
|
|
280
|
-
function claudePolicy(event, script) {
|
|
281
|
-
return `bash -lc 'root="\${CLAUDE_PROJECT_DIR:-$(pwd)}"; node "$root/scripts/hooks/claude-hook-adapter.js" ${event} ${script.replace(/\.js$/, "")} ${script} default'`;
|
|
281
|
+
function claudePolicy(event, script, envPrefix = "") {
|
|
282
|
+
return `bash -lc 'root="\${CLAUDE_PROJECT_DIR:-$(pwd)}"; ${envPrefix}node "$root/scripts/hooks/claude-hook-adapter.js" ${event} ${script.replace(/\.js$/, "")} ${script} default'`;
|
|
282
283
|
}
|
|
283
284
|
function codexRoot(scriptPath) {
|
|
284
285
|
return `root="\${CODEX_HOME:-}"; if [ -z "$root" ] || [ ! -f "$root/${scriptPath}" ]; then root=$(git rev-parse --show-toplevel 2>/dev/null || pwd); fi`;
|
|
@@ -288,17 +289,23 @@ function codexTelemetry(event) {
|
|
|
288
289
|
return `bash -lc '${codexRoot("scripts/telemetry/telemetry.sh")}; bash "$root/scripts/telemetry/telemetry.sh" permissionRequest dev'`;
|
|
289
290
|
return `bash -lc '${codexRoot("scripts/hooks/codex-telemetry-hook.js")}; node "$root/scripts/hooks/codex-telemetry-hook.js" ${event} dev'`;
|
|
290
291
|
}
|
|
291
|
-
function codexPolicy(script) {
|
|
292
|
-
return `bash -lc '${codexRoot("scripts/hooks/codex-hook-adapter.js")}; node "$root/scripts/hooks/codex-hook-adapter.js" ${script.replace(/\.js$/, "")} ${script} default'`;
|
|
292
|
+
function codexPolicy(script, envPrefix = "") {
|
|
293
|
+
return `bash -lc '${codexRoot("scripts/hooks/codex-hook-adapter.js")}; ${envPrefix}node "$root/scripts/hooks/codex-hook-adapter.js" ${script.replace(/\.js$/, "")} ${script} default'`;
|
|
293
294
|
}
|
|
295
|
+
// Shipped L2 runtimes enforce goal fit by default (mode=block), while remaining
|
|
296
|
+
// operator-overridable via the FLOW_AGENTS_GOAL_FIT_MODE environment variable.
|
|
297
|
+
// The canonical engine default stays warn so the conformance contract is honest.
|
|
298
|
+
const GOAL_FIT_MODE_PREFIX = 'FLOW_AGENTS_GOAL_FIT_MODE="${FLOW_AGENTS_GOAL_FIT_MODE:-block}" ';
|
|
294
299
|
function exportClaudeSettings() {
|
|
295
300
|
const hooks = {};
|
|
296
301
|
for (const event of ["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "PostToolUse", "Stop", "SessionEnd"]) {
|
|
297
302
|
hooks[event] = [{ hooks: [shellHook(claudeTelemetry(event), 10, "Recording Flow Agents telemetry")] }];
|
|
298
303
|
}
|
|
299
|
-
hooks.Stop.push({ hooks: [shellHook(claudePolicy("Stop", "stop-goal-fit.js"), 30, "Running Flow Agents hook policy")] });
|
|
304
|
+
hooks.Stop.push({ hooks: [shellHook(claudePolicy("Stop", "stop-goal-fit.js", GOAL_FIT_MODE_PREFIX), 30, "Running Flow Agents hook policy")] });
|
|
305
|
+
hooks.SessionStart.push({ hooks: [shellHook(claudePolicy("SessionStart", "workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
|
|
300
306
|
hooks.UserPromptSubmit.push({ hooks: [shellHook(claudePolicy("UserPromptSubmit", "workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
|
|
301
307
|
hooks.PostToolUse.push({ hooks: [shellHook(claudePolicy("PostToolUse", "quality-gate.js"), 30, "Running Flow Agents hook policy")] });
|
|
308
|
+
hooks.PostToolUse.push({ hooks: [shellHook(claudePolicy("PostToolUse", "evidence-capture.js"), 30, "Capturing Flow Agents command evidence")] });
|
|
302
309
|
hooks.PreToolUse.push({ hooks: [shellHook(claudePolicy("PreToolUse", "config-protection.js"), 30, "Running Flow Agents hook policy")] });
|
|
303
310
|
return `${JSON.stringify({
|
|
304
311
|
statusLine: { type: "command", command: 'bash -lc \'root="${CLAUDE_PROJECT_DIR:-$(pwd)}"; node "$root/scripts/statusline/flow-agents-statusline.js"\'' },
|
|
@@ -312,8 +319,10 @@ function exportCodexHooks() {
|
|
|
312
319
|
for (const event of ["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "PostToolUse", "Stop"]) {
|
|
313
320
|
hooks[event] = [{ hooks: [shellHook(codexTelemetry(event), 10, "Recording Flow Agents telemetry")] }];
|
|
314
321
|
}
|
|
315
|
-
hooks.Stop.push({ hooks: [shellHook(codexPolicy("stop-goal-fit.js"), 30, "Running Flow Agents hook policy")] });
|
|
322
|
+
hooks.Stop.push({ hooks: [shellHook(codexPolicy("stop-goal-fit.js", GOAL_FIT_MODE_PREFIX), 30, "Running Flow Agents hook policy")] });
|
|
323
|
+
hooks.SessionStart.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
|
|
316
324
|
hooks.UserPromptSubmit.push({ hooks: [shellHook(codexPolicy("workflow-steering.js"), 30, "Running Flow Agents hook policy")] });
|
|
325
|
+
hooks.PostToolUse.push({ hooks: [shellHook(codexPolicy("evidence-capture.js"), 30, "Capturing Flow Agents command evidence")] });
|
|
317
326
|
return `${JSON.stringify({ hooks }, null, 2)}\n`;
|
|
318
327
|
}
|
|
319
328
|
function copySharedContent(targetRoot, targetName, token) {
|
|
@@ -337,21 +346,23 @@ function copySharedContent(targetRoot, targetName, token) {
|
|
|
337
346
|
for (const dir of manifest.optional_copy_dirs ?? [])
|
|
338
347
|
copyTree(path.join(root, dir), path.join(targetRoot, dir), targetName, token);
|
|
339
348
|
writeText(path.join(targetRoot, "build/package.json"), `${JSON.stringify({ type: "module" }, null, 2)}\n`);
|
|
340
|
-
const filterBuilt = path.join(root, "build/src/tools/filter-installed-packs.js");
|
|
341
349
|
const commonBuilt = path.join(root, "build/src/tools/common.js");
|
|
342
|
-
if (fs.existsSync(filterBuilt))
|
|
343
|
-
writeText(path.join(targetRoot, "scripts/filter-installed-packs.mjs"), readText(filterBuilt).replace("./common.js", "./common.mjs"));
|
|
344
350
|
if (fs.existsSync(commonBuilt))
|
|
345
351
|
writeText(path.join(targetRoot, "scripts/common.mjs"), readText(commonBuilt));
|
|
346
352
|
copyTree(path.join(root, "build/src"), path.join(targetRoot, "build/src"), targetName, token);
|
|
347
353
|
}
|
|
348
|
-
function installScript(label, defaultDestDisplay, token, destFallbackShell) {
|
|
354
|
+
function installScript(label, defaultDestDisplay, token, destFallbackShell, mergeConfig, stampConfig) {
|
|
349
355
|
const replaceBlock = token ? `\nexport DEST\nfind "$DEST" -type f \\( -name '*.json' -o -name '*.md' -o -name '*.sh' -o -name '*.js' -o -name '*.ts' -o -name '*.yaml' -o -name '*.yml' \\) -print0 | xargs -0 perl -0pi -e 's#${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}#$ENV{DEST}#g'` : "";
|
|
350
356
|
const destFallback = destFallbackShell ? `\nif [[ -z "$DEST" ]]; then\n DEST="${destFallbackShell}"\nfi` : "";
|
|
351
357
|
const destRequired = !destFallbackShell;
|
|
352
358
|
const requiredCheck = destRequired ? `if [[ -z "$DEST" ]]; then\n usage\n exit 2\nfi\n` : "";
|
|
353
359
|
const usageDest = destRequired ? "/path/to/workspace" : defaultDestDisplay;
|
|
354
|
-
|
|
360
|
+
const mergeBlock = mergeConfig
|
|
361
|
+
? `\nif command -v node >/dev/null 2>&1; then\n node "$DEST/scripts/install-merge.js" --config "$DEST/${mergeConfig.configRelPath}" --managed-hooks "$SRC/${mergeConfig.managedConfigRelPath}" --version "${mergeConfig.version}" --install-record "$DEST/.flow-agents/install.json" --runtime "${mergeConfig.runtime}" || true\nfi`
|
|
362
|
+
: stampConfig
|
|
363
|
+
? `\nif command -v node >/dev/null 2>&1; then\n node "$DEST/scripts/install-merge.js" --stamp-only --version "${stampConfig.version}" --install-record "$DEST/.flow-agents/install.json" --runtime "${stampConfig.runtime}" || true\nfi`
|
|
364
|
+
: "";
|
|
365
|
+
return `#!/usr/bin/env bash\nset -euo pipefail\n\nusage() {\n cat >&2 <<'EOF'\nusage: bash install.sh ${usageDest} [options]\n\nOptions:\n --telemetry-sink NAME local-files, local-kontour-console,\n kontour-hosted-console, user-hosted-console,\n or legacy aliases. May be repeated.\n --console-url URL Persist Console telemetry base URL.\n --console-endpoint URL Persist full Console telemetry records endpoint URL.\n --console-token-file PATH\n Read Console telemetry bearer token from a file.\n --console-tenant ID Persist Console tenant identifier.\nEOF\n}\n\nDEST=""\nDEST_SET=0\nCONSOLE_CONFIG_ARGS=()\nwhile [[ $# -gt 0 ]]; do\n case "$1" in\n --telemetry-sink|--telemetry-sinks|--console-url|--console-endpoint|--console-endpoint-url|--console-token-file|--console-tenant|--console-tenant-id)\n [[ $# -ge 2 ]] || { echo "install.sh: $1 requires a value" >&2; exit 2; }\n CONSOLE_CONFIG_ARGS+=("$1" "$2")\n shift 2\n ;;\n --help|-h)\n usage\n exit 0\n ;;\n -*)\n echo "install.sh: unknown option: $1" >&2\n usage\n exit 2\n ;;\n *)\n if [[ "$DEST_SET" -eq 1 ]]; then\n echo "install.sh: unexpected argument: $1" >&2\n usage\n exit 2\n fi\n DEST="$1"\n DEST_SET=1\n shift\n ;;\n esac\ndone${destFallback}\n${requiredCheck}SRC="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"\n\nmkdir -p "$DEST"\nrsync -a ${token ? "--delete " : ""}${mergeConfig ? `--exclude="${mergeConfig.configRelPath}" ` : ""}"$SRC"/ "$DEST"/${replaceBlock}${mergeBlock}\nif [[ \${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "\${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "\${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "\${FLOW_AGENTS_CONSOLE_URL:-}" || -n "\${CONSOLE_TELEMETRY_URL:-}" || -n "\${CONSOLE_URL:-}" || -n "\${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "\${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then\n bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "\${CONSOLE_CONFIG_ARGS[@]}"\nfi\necho "Installed ${label} bundle ${token ? "to" : "into"} $DEST"\n`;
|
|
355
366
|
}
|
|
356
367
|
function buildBase(agents) {
|
|
357
368
|
const bundle = path.join(dist, "base");
|
|
@@ -360,7 +371,7 @@ function buildBase(agents) {
|
|
|
360
371
|
writeText(path.join(bundle, ".flow-agents", ".gitkeep"), "");
|
|
361
372
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Base", agents, ".flow-agents"));
|
|
362
373
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("Base", "bash install.sh /path/to/workspace"));
|
|
363
|
-
writeText(path.join(bundle, "install.sh"), installScript("Base", "/path/to/workspace"));
|
|
374
|
+
writeText(path.join(bundle, "install.sh"), installScript("Base", "/path/to/workspace", undefined, undefined, undefined, { runtime: "base", version: pkgVersion }));
|
|
364
375
|
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
365
376
|
}
|
|
366
377
|
function buildKiro(agents) {
|
|
@@ -372,7 +383,7 @@ function buildKiro(agents) {
|
|
|
372
383
|
writeText(path.join(bundle, "agents", `${spec.name}.json`), sanitizeText(`${JSON.stringify(sanitizeAgentJson(spec), null, 2)}\n`, "kiro", token));
|
|
373
384
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Kiro", agents, ".flow-agents"));
|
|
374
385
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("Kiro", "bash install.sh $HOME/.flow-agents"));
|
|
375
|
-
writeText(path.join(bundle, "install.sh"), installScript("Kiro", "$HOME/.flow-agents", token, '${FLOW_AGENTS_DEST:-$HOME/.flow-agents}'));
|
|
386
|
+
writeText(path.join(bundle, "install.sh"), installScript("Kiro", "$HOME/.flow-agents", token, '${FLOW_AGENTS_DEST:-$HOME/.flow-agents}', undefined, { runtime: "kiro", version: pkgVersion }));
|
|
376
387
|
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
377
388
|
}
|
|
378
389
|
function buildClaudeCode(agents) {
|
|
@@ -388,7 +399,7 @@ function buildClaudeCode(agents) {
|
|
|
388
399
|
writeText(path.join(bundle, ".claude/settings.json"), exportClaudeSettings());
|
|
389
400
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Claude Code", agents, manifest.claude_code.task_dir));
|
|
390
401
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("Claude Code", "bash install.sh /path/to/workspace"));
|
|
391
|
-
writeText(path.join(bundle, "install.sh"), installScript("Claude Code", "/path/to/workspace"));
|
|
402
|
+
writeText(path.join(bundle, "install.sh"), installScript("Claude Code", "/path/to/workspace", undefined, undefined, { configRelPath: ".claude/settings.json", managedConfigRelPath: ".claude/settings.json", runtime: "claude-code", version: pkgVersion }));
|
|
392
403
|
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
393
404
|
}
|
|
394
405
|
function buildCodex(agents) {
|
|
@@ -409,8 +420,8 @@ function buildCodex(agents) {
|
|
|
409
420
|
writeText(path.join(bundle, ".codex/skills", name, "SKILL.md"), sanitizeText(readText(src), "codex", "<bundle-root>"));
|
|
410
421
|
}
|
|
411
422
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
|
|
412
|
-
writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace"));
|
|
413
|
-
writeText(path.join(bundle, "install.sh"), installScript("Codex", "/path/to/workspace"));
|
|
423
|
+
writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace", CODEX_LIVE_HOOKS_README));
|
|
424
|
+
writeText(path.join(bundle, "install.sh"), installScript("Codex", "/path/to/workspace", undefined, undefined, { configRelPath: ".codex/hooks.json", managedConfigRelPath: ".codex/hooks.json", runtime: "codex", version: pkgVersion }));
|
|
414
425
|
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
415
426
|
}
|
|
416
427
|
function exportOpencodeAgent(spec) {
|
|
@@ -546,6 +557,7 @@ export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree
|
|
|
546
557
|
const detail = { tool: input && input.tool };
|
|
547
558
|
runTelemetry('tool.execute.after', detail);
|
|
548
559
|
runAdapter('opencode-hook-adapter.js', 'tool.execute.after', detail, 'quality-gate', 'quality-gate.js', 'default');
|
|
560
|
+
runAdapter('opencode-hook-adapter.js', 'tool.execute.after', detail, 'evidence-capture', 'evidence-capture.js', 'default');
|
|
549
561
|
},
|
|
550
562
|
'session.idle': async (_input, _output) => {
|
|
551
563
|
runTelemetry('session.idle');
|
|
@@ -591,7 +603,7 @@ function buildOpencode(agents) {
|
|
|
591
603
|
writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
|
|
592
604
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("opencode", agents, manifest.opencode.task_dir));
|
|
593
605
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("opencode", "bash install.sh /path/to/workspace"));
|
|
594
|
-
writeText(path.join(bundle, "install.sh"), installScript("opencode", "/path/to/workspace"));
|
|
606
|
+
writeText(path.join(bundle, "install.sh"), installScript("opencode", "/path/to/workspace", undefined, undefined, { configRelPath: "opencode.json", managedConfigRelPath: "opencode.json", runtime: "opencode", version: pkgVersion }));
|
|
595
607
|
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
596
608
|
}
|
|
597
609
|
function exportPiExtension() {
|
|
@@ -679,6 +691,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
679
691
|
pi.on("tool_result", async (_event, _ctx) => {
|
|
680
692
|
runTelemetry("tool_result");
|
|
681
693
|
runAdapter("pi-hook-adapter.js", "tool_result", "quality-gate", "quality-gate.js");
|
|
694
|
+
runAdapter("pi-hook-adapter.js", "tool_result", "evidence-capture", "evidence-capture.js");
|
|
682
695
|
});
|
|
683
696
|
|
|
684
697
|
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
@@ -701,7 +714,7 @@ function buildPi(agents) {
|
|
|
701
714
|
writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
|
|
702
715
|
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
|
|
703
716
|
writeText(path.join(bundle, "README.md"), exportTargetReadme("pi", "bash install.sh /path/to/workspace"));
|
|
704
|
-
writeText(path.join(bundle, "install.sh"), installScript("pi", "/path/to/workspace"));
|
|
717
|
+
writeText(path.join(bundle, "install.sh"), installScript("pi", "/path/to/workspace", undefined, undefined, undefined, { runtime: "pi", version: pkgVersion }));
|
|
705
718
|
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
706
719
|
}
|
|
707
720
|
function buildCatalog(agents) {
|
|
@@ -711,7 +724,6 @@ function buildCatalog(agents) {
|
|
|
711
724
|
agents: agents.slice().sort((a, b) => a.name.localeCompare(b.name)).map((spec) => spec.name),
|
|
712
725
|
skills: collectAllSkills().map(({ name }) => name),
|
|
713
726
|
powers: fs.readdirSync(path.join(root, "powers")).filter((name) => fs.existsSync(path.join(root, "powers", name, "mcp.json"))).sort(),
|
|
714
|
-
packs: packs.packs ?? [],
|
|
715
727
|
kits: fs.existsSync(kitsCatalog) ? loadJson(kitsCatalog).kits ?? [] : [],
|
|
716
728
|
};
|
|
717
729
|
}
|