@kontourai/flow-agents 3.3.0 → 3.4.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/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +48 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +56 -3
- package/build/src/lib/flow-resolver.js +151 -11
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +101 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +153 -10
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +26 -1
package/src/lib/fs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
3
4
|
|
|
4
5
|
export function readJson(file: string): unknown {
|
|
5
6
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -10,6 +11,118 @@ export function writeJson(file: string, value: unknown): void {
|
|
|
10
11
|
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
function existingAncestor(file: string): { ancestor: string; missing: string[] } {
|
|
15
|
+
let current = path.resolve(file);
|
|
16
|
+
const missing: string[] = [];
|
|
17
|
+
while (!fs.existsSync(current)) {
|
|
18
|
+
missing.unshift(path.basename(current));
|
|
19
|
+
const parent = path.dirname(current);
|
|
20
|
+
if (parent === current) break;
|
|
21
|
+
current = parent;
|
|
22
|
+
}
|
|
23
|
+
return { ancestor: current, missing };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolve a possibly-missing path through its nearest existing ancestor. */
|
|
27
|
+
export function canonicalProspectivePath(file: string): string {
|
|
28
|
+
const { ancestor, missing } = existingAncestor(file);
|
|
29
|
+
return path.resolve(fs.realpathSync(ancestor), ...missing);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isContained(root: string, target: string): boolean {
|
|
33
|
+
const relative = path.relative(root, target);
|
|
34
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Reject equality and either-direction containment after canonical resolution. */
|
|
38
|
+
export function assertPathsDisjoint(first: string, second: string): void {
|
|
39
|
+
const firstReal = canonicalProspectivePath(first);
|
|
40
|
+
const secondReal = canonicalProspectivePath(second);
|
|
41
|
+
if (isContained(firstReal, secondReal) || isContained(secondReal, firstReal)) {
|
|
42
|
+
throw new Error(`refusing overlapping paths: ${firstReal} and ${secondReal}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a directory below root without following any symlink that already
|
|
48
|
+
* exists at or below the root. Ambient aliases above root (for example macOS
|
|
49
|
+
* /tmp -> /private/tmp) are resolved once and are not treated as destination
|
|
50
|
+
* components.
|
|
51
|
+
*/
|
|
52
|
+
export function ensureSafeDirectory(root: string, directory: string): string {
|
|
53
|
+
const rootPath = path.resolve(root);
|
|
54
|
+
if (fs.existsSync(rootPath)) {
|
|
55
|
+
const rootStat = fs.lstatSync(rootPath);
|
|
56
|
+
if (rootStat.isSymbolicLink()) throw new Error(`refusing symlink destination root: ${rootPath}`);
|
|
57
|
+
if (!rootStat.isDirectory()) throw new Error(`destination root is not a directory: ${rootPath}`);
|
|
58
|
+
} else {
|
|
59
|
+
const { ancestor, missing } = existingAncestor(rootPath);
|
|
60
|
+
let current = fs.realpathSync(ancestor);
|
|
61
|
+
for (const part of missing) {
|
|
62
|
+
current = path.join(current, part);
|
|
63
|
+
if (fs.existsSync(current)) {
|
|
64
|
+
const stat = fs.lstatSync(current);
|
|
65
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing to write through symlink: ${current}`);
|
|
66
|
+
if (!stat.isDirectory()) throw new Error(`destination component is not a directory: ${current}`);
|
|
67
|
+
} else {
|
|
68
|
+
fs.mkdirSync(current);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const rootReal = fs.realpathSync(rootPath);
|
|
74
|
+
const target = path.resolve(directory);
|
|
75
|
+
const prospective = canonicalProspectivePath(target);
|
|
76
|
+
if (!isContained(rootReal, prospective)) throw new Error(`path escapes root: ${target}`);
|
|
77
|
+
const relative = path.relative(rootReal, prospective);
|
|
78
|
+
let current = rootReal;
|
|
79
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
80
|
+
current = path.join(current, part);
|
|
81
|
+
if (fs.existsSync(current)) {
|
|
82
|
+
const stat = fs.lstatSync(current);
|
|
83
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing to write through symlink: ${current}`);
|
|
84
|
+
if (!stat.isDirectory()) throw new Error(`destination component is not a directory: ${current}`);
|
|
85
|
+
} else {
|
|
86
|
+
fs.mkdirSync(current);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return prospective;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertSafeFileTarget(root: string, file: string): string {
|
|
93
|
+
const target = path.resolve(file);
|
|
94
|
+
ensureSafeDirectory(root, path.dirname(target));
|
|
95
|
+
const rootReal = fs.realpathSync(path.resolve(root));
|
|
96
|
+
const prospective = canonicalProspectivePath(target);
|
|
97
|
+
if (!isContained(rootReal, prospective) || prospective === rootReal) throw new Error(`path escapes root: ${target}`);
|
|
98
|
+
if (fs.existsSync(target)) {
|
|
99
|
+
const stat = fs.lstatSync(target);
|
|
100
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing to replace symlink: ${target}`);
|
|
101
|
+
if (!stat.isFile()) throw new Error(`destination is not a regular file: ${target}`);
|
|
102
|
+
}
|
|
103
|
+
return target;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Atomically replace a regular file without following a final-component symlink. */
|
|
107
|
+
export function atomicWriteFile(root: string, file: string, data: string | Buffer): void {
|
|
108
|
+
const target = assertSafeFileTarget(root, file);
|
|
109
|
+
const temp = path.join(path.dirname(target), `.${path.basename(target)}.flow-agents-${process.pid}-${crypto.randomBytes(6).toString("hex")}.tmp`);
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(temp, data, { flag: "wx" });
|
|
112
|
+
fs.renameSync(temp, target);
|
|
113
|
+
} finally {
|
|
114
|
+
fs.rmSync(temp, { force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function atomicWriteJson(root: string, file: string, value: unknown): void {
|
|
119
|
+
atomicWriteFile(root, file, `${JSON.stringify(value, null, 2)}\n`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function atomicCopyFile(root: string, source: string, target: string): void {
|
|
123
|
+
atomicWriteFile(root, target, fs.readFileSync(source));
|
|
124
|
+
}
|
|
125
|
+
|
|
13
126
|
export function copyDir(src: string, dest: string): void {
|
|
14
127
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
15
128
|
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
@@ -19,6 +132,53 @@ export function copyDir(src: string, dest: string): void {
|
|
|
19
132
|
});
|
|
20
133
|
}
|
|
21
134
|
|
|
135
|
+
function copiedTreeDigest(root: string): string {
|
|
136
|
+
const hash = crypto.createHash("sha256");
|
|
137
|
+
for (const file of walkFiles(root)) {
|
|
138
|
+
const rel = path.relative(root, file).split(path.sep).join("/");
|
|
139
|
+
if (rel.split("/").some((part) => [".git", "__pycache__", ".pytest_cache"].includes(part))) continue;
|
|
140
|
+
hash.update(rel);
|
|
141
|
+
hash.update("\0");
|
|
142
|
+
hash.update(fs.readFileSync(file));
|
|
143
|
+
hash.update("\0");
|
|
144
|
+
}
|
|
145
|
+
return hash.digest("hex");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Copy through a verified sibling directory, then swap with rollback. */
|
|
149
|
+
export function copyDirAtomic(root: string, src: string, dest: string): void {
|
|
150
|
+
assertPathsDisjoint(src, dest);
|
|
151
|
+
const parent = ensureSafeDirectory(root, path.dirname(dest));
|
|
152
|
+
if (fs.existsSync(dest)) {
|
|
153
|
+
const stat = fs.lstatSync(dest);
|
|
154
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing to replace symlink: ${dest}`);
|
|
155
|
+
if (!stat.isDirectory()) throw new Error(`destination is not a directory: ${dest}`);
|
|
156
|
+
}
|
|
157
|
+
const nonce = `${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
158
|
+
const temp = path.join(parent, `.${path.basename(dest)}.flow-agents-${nonce}.tmp`);
|
|
159
|
+
const backup = path.join(parent, `.${path.basename(dest)}.flow-agents-${nonce}.old`);
|
|
160
|
+
try {
|
|
161
|
+
fs.cpSync(src, temp, {
|
|
162
|
+
recursive: true,
|
|
163
|
+
errorOnExist: true,
|
|
164
|
+
force: false,
|
|
165
|
+
filter: (source) => !source.split(path.sep).some((part) => [".git", "__pycache__", ".pytest_cache"].includes(part)),
|
|
166
|
+
});
|
|
167
|
+
if (copiedTreeDigest(src) !== copiedTreeDigest(temp)) throw new Error(`copied kit verification failed: ${src}`);
|
|
168
|
+
if (fs.existsSync(dest)) fs.renameSync(dest, backup);
|
|
169
|
+
try {
|
|
170
|
+
fs.renameSync(temp, dest);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (fs.existsSync(backup)) fs.renameSync(backup, dest);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
176
|
+
} finally {
|
|
177
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
178
|
+
if (fs.existsSync(backup) && !fs.existsSync(dest)) fs.renameSync(backup, dest);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
22
182
|
export function assertPathContained(root: string, target: string): void {
|
|
23
183
|
const rootReal = fs.realpathSync(root);
|
|
24
184
|
let existingParent = path.dirname(target);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
|
|
5
6
|
export const KONTOURAI_DIR = ".kontourai";
|
|
6
7
|
export const FLOW_AGENTS_RUNTIME_SUBDIR = "flow-agents";
|
|
@@ -16,12 +17,132 @@ export const LEGACY_TELEMETRY_DIR = ".telemetry";
|
|
|
16
17
|
* and the durable per-destination install record root
|
|
17
18
|
* (`DURABLE_FLOW_AGENTS_DIR`, i.e. `.flow-agents`).
|
|
18
19
|
*/
|
|
19
|
-
export function defaultCodexHome(): string {
|
|
20
|
-
return
|
|
20
|
+
export function defaultCodexHome(env: NodeJS.ProcessEnv = process.env, homedir: string = os.homedir()): string {
|
|
21
|
+
return env["CODEX_HOME"] || path.join(homedir, ".codex");
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* #357: resolve the SHARED, git-common-dir-anchored `.kontourai/flow-agents` root for `cwd`.
|
|
26
|
+
*
|
|
27
|
+
* `git rev-parse --git-common-dir` returns the ONE `.git` directory shared by every worktree
|
|
28
|
+
* of a repository (in the primary checkout this is simply `.git` relative to cwd; in a linked
|
|
29
|
+
* worktree it resolves to the primary checkout's real `.git` directory, e.g.
|
|
30
|
+
* `/path/to/primary/.git`, NOT the worktree's own `.git` file). Taking `path.dirname()` of that
|
|
31
|
+
* resolved, absolute path yields the primary checkout's repo root — the one location every
|
|
32
|
+
* worktree of the same repo should treat as the shared `.kontourai/flow-agents` store, so a
|
|
33
|
+
* `liveness claim` or `ensure-session` invoked from ANY worktree's cwd is visible to a reader
|
|
34
|
+
* in any other checkout of the same repo (including the primary one).
|
|
35
|
+
*
|
|
36
|
+
* Returns null (never throws) when git is unavailable, `cwd` is not inside a git working tree,
|
|
37
|
+
* or the command otherwise fails — callers MUST fail open to the existing cwd-based
|
|
38
|
+
* `path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR)` behavior in that case (see flowAgentsArtifactRoot
|
|
39
|
+
* below), which is also exactly what happens today in the single-checkout case: there,
|
|
40
|
+
* `--git-common-dir` resolves to `.git` under cwd itself, so `path.dirname()` of its absolute
|
|
41
|
+
* form is cwd — byte-identical to today's plain `path.resolve(cwd, ...)` result.
|
|
42
|
+
*
|
|
43
|
+
* #413 iteration-2 Fix 3: strips the ambient GIT_DIR/GIT_COMMON_DIR/GIT_WORK_TREE/
|
|
44
|
+
* GIT_CEILING_DIRECTORIES env vars before shelling out. Any of these, if set in the calling
|
|
45
|
+
* process's environment (e.g. leaked from an outer `git rebase`/hook invocation, or a stray
|
|
46
|
+
* export in a dev shell), can silently redirect `--git-common-dir` to resolve a DIFFERENT
|
|
47
|
+
* repository's `.git` than the one `cwd` is actually inside — which would silently redirect
|
|
48
|
+
* the shared `.kontourai/flow-agents` store to that other repo. Passing an explicit env with
|
|
49
|
+
* those keys deleted makes resolution depend only on `cwd`, never on ambient process state.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveSharedRepoRoot(cwd: string): string | null {
|
|
52
|
+
try {
|
|
53
|
+
const env = { ...process.env };
|
|
54
|
+
delete env["GIT_DIR"];
|
|
55
|
+
delete env["GIT_COMMON_DIR"];
|
|
56
|
+
delete env["GIT_WORK_TREE"];
|
|
57
|
+
delete env["GIT_CEILING_DIRECTORIES"];
|
|
58
|
+
const out = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
59
|
+
cwd,
|
|
60
|
+
env,
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
63
|
+
}).trim();
|
|
64
|
+
if (!out) return null;
|
|
65
|
+
const absoluteCommonDir = path.resolve(cwd, out);
|
|
66
|
+
return path.dirname(absoluteCommonDir);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* #413 iteration-2 Fix 1: cheap discriminator for the loud-vs-silent fail-open decision below.
|
|
74
|
+
* Walks up from `cwd` (bounded, same 40-level cap the hook-side ancestor walks use) looking for
|
|
75
|
+
* a `.git` entry — a FILE (linked worktree's gitdir pointer) or a DIRECTORY (primary checkout) —
|
|
76
|
+
* without shelling out to git at all, so it stays meaningful even when git itself is the thing
|
|
77
|
+
* that's broken/absent. Never throws; a filesystem error at any level is treated as "no .git
|
|
78
|
+
* found here", not as a crash.
|
|
79
|
+
*/
|
|
80
|
+
function isInsideGitWorkingTree(cwd: string): boolean {
|
|
81
|
+
try {
|
|
82
|
+
let dir = path.resolve(cwd);
|
|
83
|
+
const root = path.parse(dir).root;
|
|
84
|
+
for (let depth = 0; depth < 40; depth++) {
|
|
85
|
+
if (fs.existsSync(path.join(dir, ".git"))) return true;
|
|
86
|
+
if (dir === root) break;
|
|
87
|
+
const parent = path.dirname(dir);
|
|
88
|
+
if (parent === dir) break;
|
|
89
|
+
dir = parent;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* #413 iteration-2 Fix 1 (HIGH, security): emit a LOUD stderr warning before falling open, but
|
|
99
|
+
* ONLY when `cwd` is genuinely inside a git working tree (per isInsideGitWorkingTree above) AND
|
|
100
|
+
* resolveSharedRepoRoot still returned null — i.e. git resolution was ATTEMPTED and FAILED, not
|
|
101
|
+
* merely "there was no git repo to resolve in the first place". That second, benign case (a
|
|
102
|
+
* scratch dir, a non-git cwd) must stay perfectly silent — this is exactly what
|
|
103
|
+
* evals/integration/test_liveness_worktree_root.sh's AC6 companion case (no git at all) already
|
|
104
|
+
* asserts, and it must remain byte-identical to today.
|
|
105
|
+
*
|
|
106
|
+
* The loud case (corrupted gitlink, bad GIT_DIR-in-cwd's-own-config, git binary missing, a
|
|
107
|
+
* permissions error on .git, etc.) is #357's exact silent-fail-open bug: an actor's
|
|
108
|
+
* liveness/ownership claim lands in an ISOLATED cwd-local store invisible to every other
|
|
109
|
+
* worktree/checkout of the same repo, with no diagnostic at all. A warning (not a hard refuse —
|
|
110
|
+
* this helper sits on read paths too, and a degraded read must still return something usable)
|
|
111
|
+
* gives an operator the signal needed to pass `--artifact-root` explicitly instead of silently
|
|
112
|
+
* losing coordination visibility.
|
|
113
|
+
*/
|
|
114
|
+
function warnIfFailingOpenInsideGitTree(cwd: string, fallbackPath: string): void {
|
|
115
|
+
if (!isInsideGitWorkingTree(cwd)) return;
|
|
116
|
+
process.stderr.write(
|
|
117
|
+
`[artifact-root] WARNING: inside a git working tree but could not resolve the shared repo root ` +
|
|
118
|
+
`(git rev-parse --git-common-dir failed or returned nothing from ${cwd}); falling back to a ` +
|
|
119
|
+
`cwd-local store at ${fallbackPath} — coordination claims may be invisible to other ` +
|
|
120
|
+
`worktrees/actors. Pass --artifact-root explicitly to fix.\n`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* #413 iteration-2 Fix 2 (documentation only, no behavior change): when `cwd` is a
|
|
126
|
+
* SUBDIRECTORY of a plain (non-worktree) checkout, `git rev-parse --git-common-dir` returns a
|
|
127
|
+
* RELATIVE path (e.g. `../.git` or deeper) that `resolveSharedRepoRoot` walks up to the repo
|
|
128
|
+
* root — so this intentionally resolves to `<repo-root>/.kontourai/flow-agents` regardless of
|
|
129
|
+
* which subdirectory `cwd` is, NOT `<subdir>/.kontourai/flow-agents` (the old plain
|
|
130
|
+
* `path.resolve(cwd, ...)` cwd-anchored behavior). This is the CORRECT shared-store semantics —
|
|
131
|
+
* one store per repo, matching every other cwd (primary checkout, linked worktree) this
|
|
132
|
+
* resolver already unifies — not a regression to guard against. See
|
|
133
|
+
* evals/integration/test_liveness_worktree_root.sh's AC6b for the explicit assertion, and its
|
|
134
|
+
* header docstring for why AC6's "byte-identical" guarantee is scoped to repo-root cwd and
|
|
135
|
+
* non-git cwd only, never to an arbitrary subdirectory cwd.
|
|
136
|
+
*/
|
|
23
137
|
export function flowAgentsArtifactRoot(cwd = process.cwd()): string {
|
|
24
|
-
|
|
138
|
+
const sharedRepoRoot = resolveSharedRepoRoot(cwd);
|
|
139
|
+
if (sharedRepoRoot) return path.resolve(sharedRepoRoot, FLOW_AGENTS_RUNTIME_DIR);
|
|
140
|
+
// Fail-open: not a git repo, git unavailable, or command failed — reproduce today's plain
|
|
141
|
+
// cwd-based behavior unchanged (AC6 backward-compat guarantee). #413 iteration-2 Fix 1: loud,
|
|
142
|
+
// not silent, when cwd IS inside a git working tree (see warnIfFailingOpenInsideGitTree).
|
|
143
|
+
const fallback = path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR);
|
|
144
|
+
warnIfFailingOpenInsideGitTree(cwd, fallback);
|
|
145
|
+
return fallback;
|
|
25
146
|
}
|
|
26
147
|
|
|
27
148
|
export function durableFlowAgentsRoot(cwd = process.cwd()): string {
|
|
@@ -32,6 +153,11 @@ export function durableInstallRecordPath(cwd = process.cwd()): string {
|
|
|
32
153
|
return path.join(durableFlowAgentsRoot(cwd), "install.json");
|
|
33
154
|
}
|
|
34
155
|
|
|
156
|
+
/** Path to the per-skill-file content-hash drift manifest, a sibling of `install.json` under the same durable root. */
|
|
157
|
+
export function skillsManifestPath(cwd = process.cwd()): string {
|
|
158
|
+
return path.join(durableFlowAgentsRoot(cwd), "skills-manifest.json");
|
|
159
|
+
}
|
|
160
|
+
|
|
35
161
|
export function telemetryDataDir(cwd = process.cwd()): string {
|
|
36
162
|
return path.resolve(cwd, KONTOURAI_DIR, "telemetry");
|
|
37
163
|
}
|
package/src/runtime-adapters.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { readJson, relPath
|
|
3
|
+
import { atomicCopyFile, atomicWriteJson, ensureSafeDirectory, readJson, relPath } from "./lib/fs.js";
|
|
4
4
|
import { flowAgentsArtifactRoot } from "./lib/local-artifact-root.js";
|
|
5
|
-
import { parseKitDependencies } from "./flow-kit/validate.js";
|
|
5
|
+
import { parseKitDependencies, parseKitHookInfluenceExpectations, parseKitWorkflowTriggers } from "./flow-kit/validate.js";
|
|
6
6
|
|
|
7
7
|
export type KitAsset = {
|
|
8
8
|
kit_id: string;
|
|
@@ -15,15 +15,52 @@ export type KitAsset = {
|
|
|
15
15
|
description?: string;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
export type
|
|
18
|
+
export type KitWorkflowTriggerRef = {
|
|
19
|
+
kit_id: string;
|
|
20
|
+
source_kind: string;
|
|
21
|
+
id: string;
|
|
22
|
+
when: string;
|
|
23
|
+
target_flow_id?: string;
|
|
24
|
+
default_skill?: string;
|
|
25
|
+
conditional_skills?: { when: string; skill: string }[];
|
|
26
|
+
required_sequence?: string[];
|
|
27
|
+
post_verify_targets?: string[];
|
|
28
|
+
};
|
|
29
|
+
export type KitHookInfluenceExpectationRef = {
|
|
30
|
+
kit_id: string;
|
|
31
|
+
source_kind: string;
|
|
32
|
+
id: string;
|
|
33
|
+
description: string;
|
|
34
|
+
tier: string;
|
|
35
|
+
hook?: string;
|
|
36
|
+
event?: string;
|
|
37
|
+
must_include_guidance: string[];
|
|
38
|
+
must_include_actions: string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type KitInventory = {
|
|
42
|
+
assets: KitAsset[];
|
|
43
|
+
dependencies: KitDependencyRef[];
|
|
44
|
+
workflow_triggers: KitWorkflowTriggerRef[];
|
|
45
|
+
hook_influence_expectations: KitHookInfluenceExpectationRef[];
|
|
46
|
+
warnings: string[];
|
|
47
|
+
errors: string[];
|
|
48
|
+
};
|
|
19
49
|
|
|
20
50
|
/** A cross-kit dependency edge discovered while loading a kit manifest. */
|
|
21
51
|
export type KitDependencyRef = { from_kit_id: string; kit_id: string; reason?: string };
|
|
22
52
|
|
|
23
53
|
/** The parsed result of a single kit manifest: its id, activatable assets, and declared dependencies. */
|
|
24
|
-
export type LoadedKit = {
|
|
54
|
+
export type LoadedKit = {
|
|
55
|
+
kit_id: string;
|
|
56
|
+
assets: KitAsset[];
|
|
57
|
+
dependencies: KitDependencyRef[];
|
|
58
|
+
workflow_triggers: KitWorkflowTriggerRef[];
|
|
59
|
+
hook_influence_expectations: KitHookInfluenceExpectationRef[];
|
|
60
|
+
};
|
|
25
61
|
|
|
26
62
|
const ASSET_CLASSES = ["flows", "skills", "docs", "adapters", "evals", "assets"];
|
|
63
|
+
const KIT_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
27
64
|
|
|
28
65
|
function assetPath(root: string, value: string): string | null {
|
|
29
66
|
if (path.isAbsolute(value)) return null;
|
|
@@ -36,20 +73,20 @@ function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[],
|
|
|
36
73
|
const manifestPath = path.join(kitRoot, "kit.json");
|
|
37
74
|
if (!fs.existsSync(manifestPath)) {
|
|
38
75
|
errors.push(`${kitRoot}: missing kit.json`);
|
|
39
|
-
return { kit_id: "", assets: [], dependencies: [] };
|
|
76
|
+
return { kit_id: "", assets: [], dependencies: [], workflow_triggers: [], hook_influence_expectations: [] };
|
|
40
77
|
}
|
|
41
78
|
let manifest: Record<string, unknown>;
|
|
42
79
|
try {
|
|
43
80
|
manifest = readJson(manifestPath) as Record<string, unknown>;
|
|
44
81
|
} catch (error) {
|
|
45
82
|
errors.push(`${manifestPath}: ${(error as Error).message}`);
|
|
46
|
-
return { kit_id: "", assets: [], dependencies: [] };
|
|
83
|
+
return { kit_id: "", assets: [], dependencies: [], workflow_triggers: [], hook_influence_expectations: [] };
|
|
47
84
|
}
|
|
48
85
|
const kitId = typeof manifest.id === "string" ? manifest.id : "";
|
|
49
86
|
const kitName = typeof manifest.name === "string" ? manifest.name : kitId;
|
|
50
|
-
if (!kitId) {
|
|
51
|
-
errors.push(`${manifestPath}:
|
|
52
|
-
return { kit_id: "", assets: [], dependencies: [] };
|
|
87
|
+
if (!KIT_ID_RE.test(kitId)) {
|
|
88
|
+
errors.push(`${manifestPath}: kit id must be a kebab-case id (^[a-z][a-z0-9-]*$)`);
|
|
89
|
+
return { kit_id: "", assets: [], dependencies: [], workflow_triggers: [], hook_influence_expectations: [] };
|
|
53
90
|
}
|
|
54
91
|
const assets: KitAsset[] = [];
|
|
55
92
|
for (const assetClass of ASSET_CLASSES) {
|
|
@@ -90,17 +127,55 @@ function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[],
|
|
|
90
127
|
// docs/adr/0019-kit-dependency-ownership.md). Shape errors surface at
|
|
91
128
|
// install/inspect/validate time via validateKitRepository; here we only collect
|
|
92
129
|
// the edges so activation can enforce presence against the full inventory.
|
|
93
|
-
const
|
|
130
|
+
const dependencyResult = parseKitDependencies(manifest, manifestPath);
|
|
131
|
+
if (dependencyResult.errors.length) {
|
|
132
|
+
errors.push(...dependencyResult.errors);
|
|
133
|
+
warnings.push(`${kitId}: invalid dependencies metadata; skipping dependencies`);
|
|
134
|
+
}
|
|
135
|
+
const dependencies: KitDependencyRef[] = dependencyResult.errors.length ? [] : dependencyResult.entries
|
|
94
136
|
.map((dep) => ({ from_kit_id: kitId, kit_id: dep.kit_id, ...(dep.reason ? { reason: dep.reason } : {}) }));
|
|
95
|
-
|
|
137
|
+
const workflowTriggerResult = parseKitWorkflowTriggers(manifest, manifestPath);
|
|
138
|
+
if (workflowTriggerResult.errors.length) {
|
|
139
|
+
errors.push(...workflowTriggerResult.errors);
|
|
140
|
+
warnings.push(`${kitId}: invalid workflow_triggers metadata; skipping workflow_triggers`);
|
|
141
|
+
}
|
|
142
|
+
const workflow_triggers: KitWorkflowTriggerRef[] = workflowTriggerResult.errors.length ? [] : workflowTriggerResult.entries
|
|
143
|
+
.map((trigger) => ({ kit_id: kitId, source_kind: sourceKind, ...trigger }));
|
|
144
|
+
const hookInfluenceResult = parseKitHookInfluenceExpectations(manifest, manifestPath);
|
|
145
|
+
if (hookInfluenceResult.errors.length) {
|
|
146
|
+
errors.push(...hookInfluenceResult.errors);
|
|
147
|
+
warnings.push(`${kitId}: invalid hook_influence_expectations metadata; skipping hook_influence_expectations`);
|
|
148
|
+
}
|
|
149
|
+
const hook_influence_expectations: KitHookInfluenceExpectationRef[] = hookInfluenceResult.errors.length ? [] : hookInfluenceResult.entries
|
|
150
|
+
.map((expectation) => ({ kit_id: kitId, source_kind: sourceKind, ...expectation }));
|
|
151
|
+
return { kit_id: kitId, assets, dependencies, workflow_triggers, hook_influence_expectations };
|
|
96
152
|
}
|
|
97
153
|
|
|
98
|
-
|
|
154
|
+
function expandKitIdFilter(requested: Set<string> | undefined, dependencies: KitDependencyRef[], presentKitIds: Set<string>): Set<string> | undefined {
|
|
155
|
+
if (!requested) return undefined;
|
|
156
|
+
const selected = new Set(requested);
|
|
157
|
+
let changed = true;
|
|
158
|
+
while (changed) {
|
|
159
|
+
changed = false;
|
|
160
|
+
for (const dep of dependencies) {
|
|
161
|
+
if (!selected.has(dep.from_kit_id) || selected.has(dep.kit_id) || !presentKitIds.has(dep.kit_id)) continue;
|
|
162
|
+
selected.add(dep.kit_id);
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return selected;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function readKitInventory(sourceRoot: string, dest: string, options: { kitIdFilter?: string[] } = {}): KitInventory {
|
|
99
170
|
const warnings: string[] = [];
|
|
100
171
|
const errors: string[] = [];
|
|
101
172
|
const assets: KitAsset[] = [];
|
|
102
173
|
const dependencies: KitDependencyRef[] = [];
|
|
174
|
+
const workflow_triggers: KitWorkflowTriggerRef[] = [];
|
|
175
|
+
const hook_influence_expectations: KitHookInfluenceExpectationRef[] = [];
|
|
176
|
+
const loadedKits: LoadedKit[] = [];
|
|
103
177
|
const presentKitIds = new Set<string>();
|
|
178
|
+
const kitIdFilter = options.kitIdFilter ? new Set(options.kitIdFilter) : undefined;
|
|
104
179
|
const catalogPath = path.join(sourceRoot, "kits", "catalog.json");
|
|
105
180
|
if (!fs.existsSync(catalogPath)) warnings.push(`${catalogPath}: built-in Kit Catalog not found; skipping built-in kits (this is normal when running outside a flow-agents checkout)`);
|
|
106
181
|
else {
|
|
@@ -117,8 +192,7 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
|
|
|
117
192
|
if (!kitRoot || !fs.existsSync(kitRoot)) warnings.push(`${catalogPath}: kits[${index}].path unavailable: ${rel}`);
|
|
118
193
|
else {
|
|
119
194
|
const loaded = loadKitAssets(kitRoot, "builtin", warnings, errors);
|
|
120
|
-
|
|
121
|
-
dependencies.push(...loaded.dependencies);
|
|
195
|
+
loadedKits.push(loaded);
|
|
122
196
|
if (loaded.kit_id) presentKitIds.add(loaded.kit_id);
|
|
123
197
|
}
|
|
124
198
|
});
|
|
@@ -138,13 +212,21 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
|
|
|
138
212
|
if (!fs.existsSync(kitRoot)) warnings.push(`${id}: installed kit copy missing at ${kitRoot}; skipping`);
|
|
139
213
|
else {
|
|
140
214
|
const loaded = loadKitAssets(kitRoot, "local", warnings, errors);
|
|
141
|
-
|
|
142
|
-
dependencies.push(...loaded.dependencies);
|
|
215
|
+
loadedKits.push(loaded);
|
|
143
216
|
if (loaded.kit_id) presentKitIds.add(loaded.kit_id);
|
|
144
217
|
}
|
|
145
218
|
});
|
|
146
219
|
}
|
|
147
220
|
}
|
|
221
|
+
const allDependencies = loadedKits.flatMap((kit) => kit.dependencies);
|
|
222
|
+
const selectedKitIds = expandKitIdFilter(kitIdFilter, allDependencies, presentKitIds);
|
|
223
|
+
for (const loaded of loadedKits) {
|
|
224
|
+
if (selectedKitIds && !selectedKitIds.has(loaded.kit_id)) continue;
|
|
225
|
+
assets.push(...loaded.assets);
|
|
226
|
+
dependencies.push(...loaded.dependencies);
|
|
227
|
+
workflow_triggers.push(...loaded.workflow_triggers);
|
|
228
|
+
hook_influence_expectations.push(...loaded.hook_influence_expectations);
|
|
229
|
+
}
|
|
148
230
|
// Activation-time cross-kit dependency enforcement: any declared dependency whose
|
|
149
231
|
// kit_id is not present among the union of built-in catalog kits and locally-installed
|
|
150
232
|
// kits is a hard error (feeds the errors[] -> activation-manifest -> non-zero exit path).
|
|
@@ -153,7 +235,7 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
|
|
|
153
235
|
errors.push(`${dep.from_kit_id}: declares a dependency on kit '${dep.kit_id}'${dep.reason ? ` (${dep.reason})` : ""} which is not installed or activated. Install it with 'flow-agents kit install <source>' and re-activate, or remove the dependency declaration if it is no longer needed.`);
|
|
154
236
|
}
|
|
155
237
|
}
|
|
156
|
-
return { assets, warnings, errors };
|
|
238
|
+
return { assets, dependencies, workflow_triggers, hook_influence_expectations, warnings, errors };
|
|
157
239
|
}
|
|
158
240
|
|
|
159
241
|
function safeSegment(value: string): string {
|
|
@@ -166,9 +248,11 @@ function safeSegment(value: string): string {
|
|
|
166
248
|
// docs: documentation markdown copied to docs/<kit-id>/ for agent reference.
|
|
167
249
|
const ACTIVATED_ASSET_CLASSES = new Set(["flows", "skills", "docs"]);
|
|
168
250
|
|
|
169
|
-
export function activateCodexLocal(sourceRoot: string, dest: string): Record<string, unknown> {
|
|
170
|
-
const inventory = readKitInventory(sourceRoot, dest);
|
|
251
|
+
export function activateCodexLocal(sourceRoot: string, dest: string, options: { kitIdFilter?: string[] } = {}): Record<string, unknown> {
|
|
252
|
+
const inventory = readKitInventory(sourceRoot, dest, options);
|
|
171
253
|
const runtimeDir = path.join(flowAgentsArtifactRoot(dest), "projections", "codex");
|
|
254
|
+
const writeRoot = path.resolve(runtimeDir, "../../..");
|
|
255
|
+
ensureSafeDirectory(writeRoot, runtimeDir);
|
|
172
256
|
const generated: Record<string, string>[] = [];
|
|
173
257
|
const skipped: Record<string, string | null>[] = [];
|
|
174
258
|
for (const asset of inventory.assets) {
|
|
@@ -178,8 +262,7 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
|
|
|
178
262
|
continue;
|
|
179
263
|
}
|
|
180
264
|
const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
|
|
181
|
-
|
|
182
|
-
fs.copyFileSync(asset.source_path, output);
|
|
265
|
+
atomicCopyFile(writeRoot, asset.source_path, output);
|
|
183
266
|
generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
|
|
184
267
|
} else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
|
|
185
268
|
// Copy skills and docs to runtime/<adapter>/<class>/<kit-id>/<filename> so the
|
|
@@ -193,18 +276,16 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
|
|
|
193
276
|
const output = asset.asset_class === "skills"
|
|
194
277
|
? path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), safeSegment(path.basename(path.dirname(asset.source_path))), filename)
|
|
195
278
|
: path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
|
|
196
|
-
|
|
197
|
-
fs.copyFileSync(asset.source_path, output);
|
|
279
|
+
atomicCopyFile(writeRoot, asset.source_path, output);
|
|
198
280
|
generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });
|
|
199
281
|
} else {
|
|
200
282
|
skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is not activated by codex-local" });
|
|
201
283
|
}
|
|
202
284
|
}
|
|
203
|
-
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
204
285
|
const supportedClasses = Array.from(ACTIVATED_ASSET_CLASSES);
|
|
205
286
|
const manifest = { schema_version: "1.0", adapter: "codex-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
|
|
206
287
|
const manifestPath = path.join(runtimeDir, "activation.json");
|
|
207
|
-
|
|
288
|
+
atomicWriteJson(writeRoot, manifestPath, manifest);
|
|
208
289
|
generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "codex-local.activation", source_path: manifestPath.split(path.sep).join("/") });
|
|
209
290
|
return { selected_adapter: "codex-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
|
|
210
291
|
}
|
|
@@ -215,13 +296,15 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
|
|
|
215
296
|
// Keeping it in the CLI adapter layer maximises reuse of readKitInventory and safeSegment,
|
|
216
297
|
// mirrors the codex-local pattern exactly, and keeps framework adapters free of catalog-
|
|
217
298
|
// layout knowledge. The Strands steering layer then reads the written runtime files.
|
|
218
|
-
export function activateStrandsLocal(sourceRoot: string, dest: string): Record<string, unknown> {
|
|
219
|
-
const inventory = readKitInventory(sourceRoot, dest);
|
|
299
|
+
export function activateStrandsLocal(sourceRoot: string, dest: string, options: { kitIdFilter?: string[] } = {}): Record<string, unknown> {
|
|
300
|
+
const inventory = readKitInventory(sourceRoot, dest, options);
|
|
220
301
|
// Runtime flows land at .kontourai/flow-agents/projections/strands/flows/<kit-id>/<asset-id>.flow.json
|
|
221
302
|
// so the Strands steering context can glob for *.flow.json under this path.
|
|
222
303
|
// Runtime skills land at .kontourai/flow-agents/projections/strands/skills/<kit-id>/<filename> and
|
|
223
304
|
// docs at .kontourai/flow-agents/projections/strands/docs/<kit-id>/<filename> for system-prompt injection.
|
|
224
305
|
const runtimeDir = path.join(flowAgentsArtifactRoot(dest), "projections", "strands");
|
|
306
|
+
const writeRoot = path.resolve(runtimeDir, "../../..");
|
|
307
|
+
ensureSafeDirectory(writeRoot, runtimeDir);
|
|
225
308
|
const generated: Record<string, string>[] = [];
|
|
226
309
|
const skipped: Record<string, string | null>[] = [];
|
|
227
310
|
for (const asset of inventory.assets) {
|
|
@@ -231,8 +314,7 @@ export function activateStrandsLocal(sourceRoot: string, dest: string): Record<s
|
|
|
231
314
|
continue;
|
|
232
315
|
}
|
|
233
316
|
const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
|
|
234
|
-
|
|
235
|
-
fs.copyFileSync(asset.source_path, output);
|
|
317
|
+
atomicCopyFile(writeRoot, asset.source_path, output);
|
|
236
318
|
generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
|
|
237
319
|
} else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
|
|
238
320
|
// Mirror the codex-local layout: strands/<class>/<kit-id>/<filename>.
|
|
@@ -246,18 +328,16 @@ export function activateStrandsLocal(sourceRoot: string, dest: string): Record<s
|
|
|
246
328
|
const output = asset.asset_class === "skills"
|
|
247
329
|
? path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), safeSegment(path.basename(path.dirname(asset.source_path))), filename)
|
|
248
330
|
: path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
|
|
249
|
-
|
|
250
|
-
fs.copyFileSync(asset.source_path, output);
|
|
331
|
+
atomicCopyFile(writeRoot, asset.source_path, output);
|
|
251
332
|
generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });
|
|
252
333
|
} else {
|
|
253
334
|
skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is not activated by strands-local" });
|
|
254
335
|
}
|
|
255
336
|
}
|
|
256
|
-
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
257
337
|
const supportedClasses = Array.from(ACTIVATED_ASSET_CLASSES);
|
|
258
338
|
const manifest = { schema_version: "1.0", adapter: "strands-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
|
|
259
339
|
const manifestPath = path.join(runtimeDir, "activation.json");
|
|
260
|
-
|
|
340
|
+
atomicWriteJson(writeRoot, manifestPath, manifest);
|
|
261
341
|
generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "strands-local.activation", source_path: manifestPath.split(path.sep).join("/") });
|
|
262
342
|
return { selected_adapter: "strands-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
|
|
263
343
|
}
|