@kontourai/flow-agents 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/build/src/builder-flow-run-adapter.d.ts +22 -2
- package/build/src/builder-flow-run-adapter.js +93 -28
- package/build/src/builder-flow-runtime.d.ts +8 -3
- package/build/src/builder-flow-runtime.js +407 -51
- package/build/src/cli/assignment-provider.d.ts +4 -7
- package/build/src/cli/assignment-provider.js +80 -14
- package/build/src/cli/builder-run.js +14 -18
- package/build/src/cli/workflow-sidecar.d.ts +28 -4
- package/build/src/cli/workflow-sidecar.js +825 -103
- package/build/src/cli/workflow.js +301 -43
- package/build/src/flow-kit/validate.d.ts +17 -0
- package/build/src/flow-kit/validate.js +340 -2
- package/build/src/index.js +5 -5
- package/build/src/lib/observed-command.d.ts +7 -0
- package/build/src/lib/observed-command.js +100 -0
- package/build/src/tools/build-universal-bundles.js +53 -3
- package/build/src/tools/generate-context-map.js +5 -3
- package/context/scripts/hooks/lib/kit-catalog.js +1 -1
- package/context/scripts/hooks/stop-goal-fit.js +78 -9
- package/docs/context-map.md +22 -20
- package/docs/developer-architecture.md +1 -1
- package/docs/getting-started.md +9 -1
- package/docs/public-workflow-cli.md +76 -7
- package/docs/skills-map.md +51 -27
- package/docs/spec/builder-flow-runtime.md +75 -40
- package/docs/workflow-usage-guide.md +109 -42
- package/evals/fixtures/hook-influence/cases.json +2 -2
- package/evals/integration/test_builder_entry_enforcement.sh +52 -41
- package/evals/integration/test_builder_step_producers.sh +330 -6
- package/evals/integration/test_bundle_install.sh +318 -65
- package/evals/integration/test_bundle_lifecycle.sh +4 -6
- package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
- package/evals/integration/test_current_json_per_actor.sh +12 -0
- package/evals/integration/test_dual_emit_flow_step.sh +62 -43
- package/evals/integration/test_flowdef_session_activation.sh +145 -25
- package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
- package/evals/integration/test_gate_lockdown.sh +3 -5
- package/evals/integration/test_goal_fit_hook.sh +60 -2
- package/evals/integration/test_install_merge.sh +18 -8
- package/evals/integration/test_liveness_verdict.sh +14 -17
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
- package/evals/integration/test_public_workflow_cli.sh +334 -14
- package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
- package/evals/integration/test_sidecar_field_preservation.sh +36 -11
- package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
- package/evals/integration/test_workflow_steering_hook.sh +15 -38
- package/evals/run.sh +2 -0
- package/evals/static/test_builder_skill_coherence.sh +247 -0
- package/evals/static/test_library_exports.sh +5 -2
- package/evals/static/test_universal_bundles.sh +44 -1
- package/evals/static/test_workflow_skills.sh +13 -325
- package/kits/builder/flows/build.flow.json +22 -0
- package/kits/builder/flows/shape.flow.json +9 -9
- package/kits/builder/kit.json +70 -16
- package/kits/builder/skills/builder-shape/SKILL.md +75 -75
- package/kits/builder/skills/continue-work/SKILL.md +45 -106
- package/kits/builder/skills/deliver/SKILL.md +96 -442
- package/kits/builder/skills/design-probe/SKILL.md +40 -139
- package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
- package/kits/builder/skills/execute-plan/SKILL.md +54 -125
- package/kits/builder/skills/fix-bug/SKILL.md +42 -132
- package/kits/builder/skills/gate-review/SKILL.md +60 -211
- package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
- package/kits/builder/skills/learning-review/SKILL.md +63 -170
- package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
- package/kits/builder/skills/plan-work/SKILL.md +51 -185
- package/kits/builder/skills/pull-work/SKILL.md +136 -485
- package/kits/builder/skills/release-readiness/SKILL.md +66 -107
- package/kits/builder/skills/review-work/SKILL.md +89 -176
- package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
- package/kits/builder/skills/verify-work/SKILL.md +101 -113
- package/package.json +2 -2
- package/packaging/README.md +1 -1
- package/scripts/README.md +1 -1
- package/scripts/hooks/lib/kit-catalog.js +1 -1
- package/scripts/hooks/stop-goal-fit.js +78 -9
- package/scripts/install-codex-home.sh +63 -23
- package/scripts/install-owned-files.js +18 -2
- package/src/builder-flow-run-adapter.ts +118 -32
- package/src/builder-flow-runtime.ts +426 -64
- package/src/cli/assignment-provider-lock.test.mjs +83 -0
- package/src/cli/assignment-provider.ts +75 -14
- package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
- package/src/cli/builder-flow-runtime.test.mjs +436 -18
- package/src/cli/builder-run.ts +3 -9
- package/src/cli/kit-metadata-security.test.mjs +232 -6
- package/src/cli/public-api.test.mjs +15 -0
- package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
- package/src/cli/workflow-sidecar.ts +746 -103
- package/src/cli/workflow.ts +288 -41
- package/src/flow-kit/validate.ts +320 -2
- package/src/index.ts +6 -5
- package/src/lib/observed-command.ts +96 -0
- package/src/tools/build-universal-bundles.ts +51 -3
- package/src/tools/generate-context-map.ts +5 -3
|
@@ -1759,6 +1759,37 @@ function hasSidecarPresence(artifactDir) {
|
|
|
1759
1759
|
return fs.existsSync(path.join(artifactDir, 'state.json')) || fs.existsSync(path.join(artifactDir, 'trust.bundle'));
|
|
1760
1760
|
}
|
|
1761
1761
|
|
|
1762
|
+
function canonicalFlowState(root, artifactDir) {
|
|
1763
|
+
if (!artifactDir) return { state: null, error: null };
|
|
1764
|
+
const slug = path.basename(artifactDir);
|
|
1765
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return { state: null, error: 'canonical Flow run slug is malformed' };
|
|
1766
|
+
const runDir = path.join(root, '.kontourai', 'flow', 'runs', slug);
|
|
1767
|
+
const components = [
|
|
1768
|
+
path.join(root, '.kontourai'),
|
|
1769
|
+
path.join(root, '.kontourai', 'flow'),
|
|
1770
|
+
path.join(root, '.kontourai', 'flow', 'runs'),
|
|
1771
|
+
runDir,
|
|
1772
|
+
];
|
|
1773
|
+
try {
|
|
1774
|
+
for (const component of components) {
|
|
1775
|
+
const stat = fs.lstatSync(component);
|
|
1776
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return { state: null, error: `canonical Flow run has an unsafe parent component: ${component}` };
|
|
1777
|
+
}
|
|
1778
|
+
const file = path.join(runDir, 'state.json');
|
|
1779
|
+
const stat = fs.lstatSync(file);
|
|
1780
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return { state: null, error: 'canonical Flow state must be a non-symlink regular file' };
|
|
1781
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1782
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)
|
|
1783
|
+
|| typeof state.status !== 'string' || !state.status.trim()
|
|
1784
|
+
|| typeof state.current_step !== 'string' || !state.current_step.trim()) {
|
|
1785
|
+
return { state: null, error: 'canonical Flow state is malformed' };
|
|
1786
|
+
}
|
|
1787
|
+
return { state, error: null };
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
return { state: null, error: `canonical Flow state is unavailable or malformed: ${safeOneLine(error && error.message || error, 120)}` };
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1762
1793
|
// WS8 (AC10a): when current.json names a slug whose session directory does NOT exist,
|
|
1763
1794
|
// return that slug so analyze() can log the staleness rather than silently falling back to
|
|
1764
1795
|
// a global mtime scan that could resurface an abandoned/never-real session as active.
|
|
@@ -1871,7 +1902,7 @@ function missingBundleOrStateSignal(artifactDir, activeFlowStep) {
|
|
|
1871
1902
|
//
|
|
1872
1903
|
// Both are used in analyze() for blocking decisions AND in run() for the AC2
|
|
1873
1904
|
// MAX_BLOCKS hard-block guard (preventing auto-release of hard blocks).
|
|
1874
|
-
const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence verdict:|evidence check .+ status:|critique status|critique open|required sidecar is missing|command-log integrity check FAILED|gate misconfiguration:|exit-code-laundered|NOT_VERIFIED \(ambiguous\)/;
|
|
1905
|
+
const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence verdict:|evidence check .+ status:|critique status|critique open|required sidecar is missing|command-log integrity check FAILED|gate misconfiguration:|exit-code-laundered|NOT_VERIFIED \(ambiguous\)|canonical Flow (?:run remains active|state is unsafe or malformed)/;
|
|
1875
1906
|
// FULL_BLOCK adds: workflow-state hygiene, surface-unavailable fail-closed, missing log.
|
|
1876
1907
|
const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action|caught false-completion|NOT_VERIFIED —|command-log integrity check FAILED|gate misconfiguration:|surface unavailable —|expected capture log is missing|exit-code-laundered|malformed-evidence|NOT_VERIFIED \(ambiguous\)/;
|
|
1877
1908
|
|
|
@@ -1897,7 +1928,31 @@ async function analyze(root, now = Date.now()) {
|
|
|
1897
1928
|
artifacts = artifacts.filter(a => a && hasSidecarPresence(path.dirname(a.file)));
|
|
1898
1929
|
}
|
|
1899
1930
|
|
|
1900
|
-
|
|
1931
|
+
const scopedCanonicalFlow = canonicalFlowState(root, scoped);
|
|
1932
|
+
const scopedState = scoped ? readJsonFile(path.join(scoped, 'state.json')) : null;
|
|
1933
|
+
const scopedProjectedActive = Boolean(scopedState && scopedState.flow_run && normalizedStatus(scopedState.flow_run.status) === 'active');
|
|
1934
|
+
if (scopedProjectedActive && scopedCanonicalFlow.error) {
|
|
1935
|
+
return {
|
|
1936
|
+
warnings: [`workflow state: canonical Flow state is unsafe or malformed for the active scoped session: ${scopedCanonicalFlow.error}. Resolve the canonical run before stopping.`],
|
|
1937
|
+
blocking: true,
|
|
1938
|
+
activeFlowRun: true,
|
|
1939
|
+
latestArtifactDir: scoped,
|
|
1940
|
+
gatePrefix: '[stop-gate]',
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
if (artifacts.length === 0) {
|
|
1944
|
+
if (normalizedStatus(scopedCanonicalFlow.state?.status) === 'active') {
|
|
1945
|
+
const activeStep = safeOneLine(scopedCanonicalFlow.state.current_step || 'unknown', 80);
|
|
1946
|
+
return {
|
|
1947
|
+
warnings: [`workflow state: canonical Flow run remains active at step ${activeStep}; complete or explicitly cancel the run before stopping.`],
|
|
1948
|
+
blocking: true,
|
|
1949
|
+
activeFlowRun: true,
|
|
1950
|
+
latestArtifactDir: scoped,
|
|
1951
|
+
gatePrefix: '[stop-gate]',
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
return { warnings: [], blocking: false, activeFlowRun: false, latestArtifactDir: null };
|
|
1955
|
+
}
|
|
1901
1956
|
|
|
1902
1957
|
const latest = artifacts[0];
|
|
1903
1958
|
const latestArtifactDir = path.dirname(latest.file);
|
|
@@ -1965,14 +2020,25 @@ async function analyze(root, now = Date.now()) {
|
|
|
1965
2020
|
// Use module-scope HARD_BLOCK / FULL_BLOCK (defined above analyze()).
|
|
1966
2021
|
// pre-execution/terminal tasks: only HARD_BLOCK signals cause a block.
|
|
1967
2022
|
// execution-onward tasks: FULL_BLOCK signals cause a block.
|
|
1968
|
-
const
|
|
2023
|
+
const canonicalFlow = canonicalFlowState(root, latestArtifactDir);
|
|
2024
|
+
const activeProjectedFlow = gateState && gateState.flow_run && normalizedStatus(gateState.flow_run.status) === 'active';
|
|
2025
|
+
const unsafeActiveCanonical = Boolean(activeProjectedFlow && canonicalFlow.error);
|
|
2026
|
+
if (unsafeActiveCanonical) {
|
|
2027
|
+
warnings.push(`workflow state: canonical Flow state is unsafe or malformed for the active scoped session: ${canonicalFlow.error}. Resolve the canonical run before stopping.`);
|
|
2028
|
+
}
|
|
2029
|
+
const activeFlowRun = normalizedStatus(canonicalFlow.state?.status) === 'active'
|
|
2030
|
+
|| (gateState && gateState.flow_run && normalizedStatus(gateState.flow_run.status) === 'active');
|
|
2031
|
+
if (activeFlowRun && !warnings.some(w => /canonical Flow run remains active/.test(w))) {
|
|
2032
|
+
const activeStep = safeOneLine(canonicalFlow.state?.current_step || gateState?.flow_run?.current_step || 'unknown', 80);
|
|
2033
|
+
warnings.push(`workflow state: canonical Flow run remains active at step ${activeStep}; complete or explicitly cancel the run before stopping.`);
|
|
2034
|
+
}
|
|
1969
2035
|
const blockRe = ((preExecution && !activeFlowRun) || terminal) ? HARD_BLOCK : FULL_BLOCK;
|
|
1970
|
-
const blocking = warnings.some(w => {
|
|
2036
|
+
const blocking = activeFlowRun || warnings.some(w => {
|
|
1971
2037
|
// Capture cross-reference warn-mode notes never block (operator opted out).
|
|
1972
2038
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
1973
2039
|
return blockRe.test(w);
|
|
1974
2040
|
});
|
|
1975
|
-
return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
|
|
2041
|
+
return { warnings, blocking, activeFlowRun, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
|
|
1976
2042
|
}
|
|
1977
2043
|
|
|
1978
2044
|
/**
|
|
@@ -2220,8 +2286,9 @@ function releaseOnNonTerminalStop(root, artifactDir) {
|
|
|
2220
2286
|
|
|
2221
2287
|
const state = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
2222
2288
|
if (!state) return; // AC5: no state.json — nothing to gate a release decision on.
|
|
2223
|
-
|
|
2224
|
-
|
|
2289
|
+
const canonicalFlow = canonicalFlowState(root, artifactDir);
|
|
2290
|
+
if (normalizedStatus(canonicalFlow.state?.status) === 'active' || (state.flow_run && normalizedStatus(state.flow_run.status) === 'active')) {
|
|
2291
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook release skipped for active Flow run "${safeOneLine(state.flow_run?.run_id || state.task_slug || path.basename(artifactDir), 80)}"; continuation remains governed by Flow state.\n`);
|
|
2225
2292
|
return;
|
|
2226
2293
|
}
|
|
2227
2294
|
|
|
@@ -2367,7 +2434,7 @@ async function run(rawInput) {
|
|
|
2367
2434
|
// with runtime-constructed paths or by modifying the warning
|
|
2368
2435
|
// text so the hash changes. The real anchor is external (signed checkpoints + human
|
|
2369
2436
|
// review). This raises the cost of the burn-through-the-counter escape vector.
|
|
2370
|
-
const isHardBlock = result.warnings.some(w => {
|
|
2437
|
+
const isHardBlock = result.activeFlowRun || result.warnings.some(w => {
|
|
2371
2438
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
2372
2439
|
return HARD_BLOCK.test(w);
|
|
2373
2440
|
});
|
|
@@ -2375,7 +2442,9 @@ async function run(rawInput) {
|
|
|
2375
2442
|
// Do NOT clear the streak — keep accumulating so the same hard block stays visible.
|
|
2376
2443
|
return {
|
|
2377
2444
|
stdout: rawInput,
|
|
2378
|
-
stderr:
|
|
2445
|
+
stderr: result.activeFlowRun
|
|
2446
|
+
? `${message}\n${gatePrefix} max-blocks reached but canonical Flow remains active — not auto-releasing; complete or explicitly cancel the run.`
|
|
2447
|
+
: `${message}\n${gatePrefix} max-blocks reached but the block is a caught false-completion / integrity failure — not auto-releasing; requires a real fix or operator override.`,
|
|
2379
2448
|
exitCode: 2,
|
|
2380
2449
|
};
|
|
2381
2450
|
}
|
|
@@ -16,14 +16,21 @@ Options:
|
|
|
16
16
|
--console-token-file PATH
|
|
17
17
|
Read Console telemetry bearer token from a file.
|
|
18
18
|
--console-tenant ID Persist Console tenant identifier.
|
|
19
|
+
--skills-dir PATH Install portable skills here (default: $HOME/.agents/skills).
|
|
19
20
|
EOF
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
DEST="${CODEX_HOME:-$HOME/.codex}"
|
|
24
|
+
SKILLS_DIR="${FLOW_AGENTS_SKILLS_DIR:-$HOME/.agents/skills}"
|
|
23
25
|
DEST_SET=0
|
|
24
26
|
CONSOLE_CONFIG_ARGS=()
|
|
25
27
|
while [[ $# -gt 0 ]]; do
|
|
26
28
|
case "$1" in
|
|
29
|
+
--skills-dir)
|
|
30
|
+
[[ $# -ge 2 ]] || { echo "install-codex-home.sh: $1 requires a value" >&2; exit 2; }
|
|
31
|
+
SKILLS_DIR="$2"
|
|
32
|
+
shift 2
|
|
33
|
+
;;
|
|
27
34
|
--telemetry-sink|--telemetry-sinks|--console-url|--console-endpoint|--console-endpoint-url|--console-token-file|--console-tenant|--console-tenant-id)
|
|
28
35
|
[[ $# -ge 2 ]] || { echo "install-codex-home.sh: $1 requires a value" >&2; exit 2; }
|
|
29
36
|
CONSOLE_CONFIG_ARGS+=("$1" "$2")
|
|
@@ -69,9 +76,26 @@ NODE
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
[[ ! -L "$DEST" ]] || { echo "install-codex-home.sh: refusing symlink destination root: $DEST" >&2; exit 1; }
|
|
79
|
+
[[ ! -L "$SKILLS_DIR" ]] || { echo "install-codex-home.sh: refusing symlink skills destination root: $SKILLS_DIR" >&2; exit 1; }
|
|
72
80
|
ROOT_REAL="$(canonicalize_path "$ROOT_DIR")"
|
|
73
81
|
BUNDLE_SOURCE_REAL="$(canonicalize_path "$ROOT_DIR/dist/codex")"
|
|
74
82
|
DEST_REAL="$(canonicalize_path "$DEST")"
|
|
83
|
+
SKILLS_DIR_REAL="$(canonicalize_path "$SKILLS_DIR")"
|
|
84
|
+
if ! node - "$SKILLS_DIR" <<'NODE'
|
|
85
|
+
const fs = require("node:fs"); const path = require("node:path");
|
|
86
|
+
const absolute = path.resolve(process.argv[2]);
|
|
87
|
+
let current = path.parse(absolute).root;
|
|
88
|
+
for (const part of absolute.slice(current.length).split(path.sep).filter(Boolean)) {
|
|
89
|
+
current = path.join(current, part);
|
|
90
|
+
// macOS exposes /tmp as the system-managed /private/tmp symlink. Treat that
|
|
91
|
+
// mount alias as trusted while rejecting caller-controlled components.
|
|
92
|
+
if (current !== "/tmp" && fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
NODE
|
|
95
|
+
then
|
|
96
|
+
echo "install-codex-home.sh: refusing symlink component in skills destination: $SKILLS_DIR" >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
75
99
|
REAL_CODEX_HOME_REAL="$(canonicalize_path "$REAL_CODEX_HOME")"
|
|
76
100
|
case "$DEST_REAL/" in
|
|
77
101
|
"$ROOT_REAL/"*|"$BUNDLE_SOURCE_REAL/"*)
|
|
@@ -85,6 +109,17 @@ case "$ROOT_REAL/" in
|
|
|
85
109
|
exit 1
|
|
86
110
|
;;
|
|
87
111
|
esac
|
|
112
|
+
case "$SKILLS_DIR_REAL/" in
|
|
113
|
+
"$ROOT_REAL/"*|"$BUNDLE_SOURCE_REAL/"*) echo "install-codex-home.sh: skills destination overlaps Flow Agents source: $SKILLS_DIR_REAL" >&2; exit 1 ;;
|
|
114
|
+
esac
|
|
115
|
+
case "$ROOT_REAL/" in
|
|
116
|
+
"$SKILLS_DIR_REAL/"*) echo "install-codex-home.sh: Flow Agents source overlaps skills destination: $SKILLS_DIR_REAL" >&2; exit 1 ;;
|
|
117
|
+
esac
|
|
118
|
+
case "$BUNDLE_SOURCE_REAL/" in
|
|
119
|
+
"$SKILLS_DIR_REAL/"*) echo "install-codex-home.sh: generated bundle source overlaps skills destination: $SKILLS_DIR_REAL" >&2; exit 1 ;;
|
|
120
|
+
esac
|
|
121
|
+
SKILLS_PATH_ROOT="$(node -e 'process.stdout.write(require("node:path").parse(process.argv[1]).root)' "$SKILLS_DIR_REAL")"
|
|
122
|
+
[[ "$SKILLS_DIR_REAL" != "$SKILLS_PATH_ROOT" ]] || { echo "install-codex-home.sh: refusing filesystem-root skills destination: $SKILLS_DIR_REAL" >&2; exit 1; }
|
|
88
123
|
|
|
89
124
|
if command -v npm >/dev/null 2>&1; then
|
|
90
125
|
(cd "$ROOT_DIR" && FLOW_AGENTS_EXPORT_DIAGNOSTICS=0 npm run build:bundles --silent >/dev/null)
|
|
@@ -93,10 +128,6 @@ else
|
|
|
93
128
|
exit 1
|
|
94
129
|
fi
|
|
95
130
|
|
|
96
|
-
mkdir -p "$DEST"
|
|
97
|
-
DEST="$(cd "$DEST" && pwd -P)"
|
|
98
|
-
DEST_REAL="$DEST"
|
|
99
|
-
|
|
100
131
|
assert_safe_dest_path() {
|
|
101
132
|
local rel="$1"
|
|
102
133
|
local current="$DEST"
|
|
@@ -122,22 +153,17 @@ assert_safe_dest_path() {
|
|
|
122
153
|
esac
|
|
123
154
|
}
|
|
124
155
|
|
|
125
|
-
# Stash the user's existing hooks.json (if any), so the merge step below can
|
|
126
|
-
# preserve user hooks across installs while replacing Flow Agents-managed groups.
|
|
127
156
|
FA_USER_HOOKS_STASH=""
|
|
128
|
-
assert_safe_dest_path "hooks.json"
|
|
129
|
-
if [[ -f "$DEST/hooks.json" ]]; then
|
|
130
|
-
FA_USER_HOOKS_STASH="$(mktemp /tmp/fa-user-hooks.XXXXXX.json)"
|
|
131
|
-
cp "$DEST/hooks.json" "$FA_USER_HOOKS_STASH"
|
|
132
|
-
fi
|
|
133
157
|
|
|
134
158
|
# A real Codex home can contain user-owned files in every shared directory.
|
|
135
159
|
# Prepare an exact Flow Agents overlay, then synchronize only files recorded in
|
|
136
160
|
# the ownership manifest. The synchronizer never uses directory-wide --delete:
|
|
137
161
|
# it removes only unchanged files from its prior manifest and refuses collisions.
|
|
138
162
|
FA_OWNED_OVERLAY="$(mktemp -d /tmp/fa-codex-overlay.XXXXXX)"
|
|
163
|
+
FA_SKILLS_OVERLAY="$(mktemp -d /tmp/fa-codex-skills-overlay.XXXXXX)"
|
|
139
164
|
cleanup_install_temps() {
|
|
140
165
|
rm -rf "$FA_OWNED_OVERLAY"
|
|
166
|
+
rm -rf "$FA_SKILLS_OVERLAY"
|
|
141
167
|
[[ -z "${FA_USER_HOOKS_STASH:-}" ]] || rm -f "$FA_USER_HOOKS_STASH"
|
|
142
168
|
}
|
|
143
169
|
trap cleanup_install_temps EXIT
|
|
@@ -157,22 +183,14 @@ for managed_dir in \
|
|
|
157
183
|
scripts
|
|
158
184
|
do
|
|
159
185
|
if [[ -d "$ROOT_DIR/dist/codex/$managed_dir" ]]; then
|
|
160
|
-
assert_safe_dest_path "$managed_dir"
|
|
161
186
|
mkdir -p "$FA_OWNED_OVERLAY/$managed_dir"
|
|
162
187
|
rsync -a "$ROOT_DIR/dist/codex/$managed_dir/" "$FA_OWNED_OVERLAY/$managed_dir/"
|
|
163
188
|
fi
|
|
164
189
|
done
|
|
165
190
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
mkdir -p "$FA_OWNED_OVERLAY/skills"
|
|
170
|
-
rsync -a "$ROOT_DIR/dist/codex/skills/" "$FA_OWNED_OVERLAY/skills/"
|
|
171
|
-
fi
|
|
172
|
-
|
|
173
|
-
if [[ -d "$ROOT_DIR/dist/codex/.codex/skills" ]]; then
|
|
174
|
-
mkdir -p "$FA_OWNED_OVERLAY/skills"
|
|
175
|
-
rsync -a "$ROOT_DIR/dist/codex/.codex/skills/" "$FA_OWNED_OVERLAY/skills/"
|
|
191
|
+
# Portable skills use Codex's universal catalog, independently of CODEX_HOME.
|
|
192
|
+
if [[ -d "$ROOT_DIR/dist/codex/.agents/skills" ]]; then
|
|
193
|
+
rsync -a "$ROOT_DIR/dist/codex/.agents/skills/" "$FA_SKILLS_OVERLAY/"
|
|
176
194
|
fi
|
|
177
195
|
|
|
178
196
|
if [[ -d "$ROOT_DIR/dist/codex/kits" ]]; then
|
|
@@ -193,10 +211,31 @@ for bundle_file in README.md console.telemetry.json install.sh; do
|
|
|
193
211
|
fi
|
|
194
212
|
done
|
|
195
213
|
|
|
196
|
-
|
|
214
|
+
# Check both destinations with the exact synchronizer before either can mutate.
|
|
215
|
+
node "$ROOT_DIR/scripts/install-owned-files.js" \
|
|
216
|
+
--check "$FA_SKILLS_OVERLAY" "$SKILLS_DIR_REAL" ".flow-agents/codex-universal-skills-install-manifest.json"
|
|
217
|
+
node "$ROOT_DIR/scripts/install-owned-files.js" \
|
|
218
|
+
--check "$FA_OWNED_OVERLAY" "$DEST_REAL" ".flow-agents/codex-install-manifest.json"
|
|
219
|
+
|
|
220
|
+
mkdir -p "$SKILLS_DIR"
|
|
221
|
+
SKILLS_DIR="$(cd "$SKILLS_DIR" && pwd -P)"
|
|
222
|
+
node "$ROOT_DIR/scripts/install-owned-files.js" \
|
|
223
|
+
"$FA_SKILLS_OVERLAY" "$SKILLS_DIR" ".flow-agents/codex-universal-skills-install-manifest.json"
|
|
224
|
+
|
|
225
|
+
mkdir -p "$DEST"
|
|
226
|
+
DEST="$(cd "$DEST" && pwd -P)"
|
|
227
|
+
DEST_REAL="$DEST"
|
|
197
228
|
node "$ROOT_DIR/scripts/install-owned-files.js" \
|
|
198
229
|
"$FA_OWNED_OVERLAY" "$DEST" ".flow-agents/codex-install-manifest.json"
|
|
199
230
|
|
|
231
|
+
# Stash the user's existing hooks.json (if any), so the merge step below can
|
|
232
|
+
# preserve user hooks across installs while replacing Flow Agents-managed groups.
|
|
233
|
+
assert_safe_dest_path "hooks.json"
|
|
234
|
+
if [[ -f "$DEST/hooks.json" ]]; then
|
|
235
|
+
FA_USER_HOOKS_STASH="$(mktemp /tmp/fa-user-hooks.XXXXXX.json)"
|
|
236
|
+
cp "$DEST/hooks.json" "$FA_USER_HOOKS_STASH"
|
|
237
|
+
fi
|
|
238
|
+
|
|
200
239
|
atomic_copy() {
|
|
201
240
|
local source="$1"
|
|
202
241
|
local rel="$2"
|
|
@@ -289,6 +328,7 @@ if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" |
|
|
|
289
328
|
fi
|
|
290
329
|
|
|
291
330
|
echo "Installed Flow Agents into Codex home at $DEST"
|
|
331
|
+
echo "Installed portable skills at $SKILLS_DIR"
|
|
292
332
|
if [[ "${#profile_names[@]}" -gt 0 ]]; then
|
|
293
333
|
echo "Profiles: ${profile_names[*]}"
|
|
294
334
|
fi
|
|
@@ -9,10 +9,24 @@ function fail(message) {
|
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const checkOnly = args[0] === "--check";
|
|
14
|
+
if (checkOnly) args.shift();
|
|
15
|
+
const [sourceArg, destArg, manifestArg] = args;
|
|
13
16
|
if (!sourceArg || !destArg || !manifestArg) fail("usage: install-owned-files.js <overlay> <destination> <manifest-relative-path>");
|
|
14
17
|
const source = fs.realpathSync(sourceArg);
|
|
15
|
-
|
|
18
|
+
function canonicalizeMissing(value) {
|
|
19
|
+
let current = path.resolve(value);
|
|
20
|
+
const missing = [];
|
|
21
|
+
while (!fs.existsSync(current)) {
|
|
22
|
+
missing.unshift(path.basename(current));
|
|
23
|
+
const parent = path.dirname(current);
|
|
24
|
+
if (parent === current) break;
|
|
25
|
+
current = parent;
|
|
26
|
+
}
|
|
27
|
+
return path.resolve(fs.realpathSync(current), ...missing);
|
|
28
|
+
}
|
|
29
|
+
const dest = canonicalizeMissing(destArg);
|
|
16
30
|
const manifestPath = path.join(dest, manifestArg);
|
|
17
31
|
|
|
18
32
|
function hashFile(file) {
|
|
@@ -132,6 +146,8 @@ for (const [rel] of previous) {
|
|
|
132
146
|
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) fail(`refusing to remove symlink replacing owned file: ${target}`);
|
|
133
147
|
}
|
|
134
148
|
|
|
149
|
+
if (checkOnly) process.exit(0);
|
|
150
|
+
|
|
135
151
|
for (const entry of incoming) {
|
|
136
152
|
const target = targetFor(entry.rel);
|
|
137
153
|
ensureSafeParent(target, true);
|