@kontourai/flow-agents 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +41 -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 +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- 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 +72 -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 +117 -4
- 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
|
@@ -18,8 +18,9 @@ const fs = require('fs');
|
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
|
|
20
20
|
const { resolveActor } = require('./lib/actor-identity');
|
|
21
|
-
const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
|
|
21
|
+
const { flowAgentsArtifactRootsForRead, resolveSharedRepoRoot, warnIfFailingOpenInsideGitTree } = require('./lib/local-artifact-paths');
|
|
22
22
|
const { readCurrentPointer } = require('./lib/current-pointer');
|
|
23
|
+
const { workflowTriggersFor } = require('./lib/kit-catalog');
|
|
23
24
|
|
|
24
25
|
const STEERING = {
|
|
25
26
|
'tool-planner': [
|
|
@@ -71,14 +72,36 @@ const ACTIVE_STATE_STATUSES = new Set([
|
|
|
71
72
|
]);
|
|
72
73
|
|
|
73
74
|
function findRepoRoot(startDir) {
|
|
74
|
-
|
|
75
|
+
const resolvedStartDir = path.resolve(startDir || process.cwd());
|
|
76
|
+
// #357: prefer git's SHARED common-dir root (the primary checkout's repo root, resolved
|
|
77
|
+
// identically from any linked worktree) before falling back to the plain `.git`/AGENTS.md
|
|
78
|
+
// ancestor walk below. Without this, a worktree's OWN `.git` FILE (not the shared `.git`
|
|
79
|
+
// DIRECTORY) satisfies `fs.existsSync(path.join(dir, '.git'))` at the very first directory
|
|
80
|
+
// checked, so the walk always stopped at the worktree root instead of resolving to the
|
|
81
|
+
// shared primary checkout — see src/lib/local-artifact-root.ts's resolveSharedRepoRoot doc
|
|
82
|
+
// comment for the full rationale. Fails open to the walk below (unchanged) when git is
|
|
83
|
+
// unavailable, `resolvedStartDir` is not inside a git working tree, or the command fails —
|
|
84
|
+
// this is also exactly today's behavior in the single-checkout case, where
|
|
85
|
+
// `--git-common-dir` resolves to `.git` under `resolvedStartDir` itself.
|
|
86
|
+
const sharedRoot = resolveSharedRepoRoot(resolvedStartDir);
|
|
87
|
+
if (sharedRoot) return sharedRoot;
|
|
88
|
+
let dir = resolvedStartDir;
|
|
75
89
|
const root = path.parse(dir).root;
|
|
76
90
|
for (let depth = 0; dir && depth < 40; depth++) {
|
|
77
|
-
if (fs.existsSync(path.join(dir, '.git'))
|
|
91
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
92
|
+
// #413 iteration-2 Fix 1: resolveSharedRepoRoot failed above even though a `.git` entry
|
|
93
|
+
// exists right here — git resolution was ATTEMPTED and FAILED (corrupted gitlink, bad
|
|
94
|
+
// GIT_DIR, git unavailable, etc.), not merely "no git repo at all". Loud, not silent —
|
|
95
|
+
// see warnIfFailingOpenInsideGitTree's own doc comment for the full rationale. This walk
|
|
96
|
+
// still returns `dir` (the ancestor-walk fallback is preserved unchanged) after warning.
|
|
97
|
+
warnIfFailingOpenInsideGitTree(resolvedStartDir, dir);
|
|
98
|
+
return dir;
|
|
99
|
+
}
|
|
100
|
+
if (fs.existsSync(path.join(dir, 'AGENTS.md'))) return dir;
|
|
78
101
|
if (dir === root) break;
|
|
79
102
|
dir = path.dirname(dir);
|
|
80
103
|
}
|
|
81
|
-
return
|
|
104
|
+
return resolvedStartDir;
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
function walkStateFiles(dir, out = []) {
|
|
@@ -244,6 +267,9 @@ function stateSteering(root) {
|
|
|
244
267
|
`STATE: ${state.task_slug || path.basename(path.dirname(current.file))} is status:${state.status} phase:${state.phase}.`,
|
|
245
268
|
];
|
|
246
269
|
if (next.summary) parts.push(`Recorded next_action.summary: "${safeStateText(next.summary)}"`);
|
|
270
|
+
if (Array.isArray(next.skills) && next.skills.length) parts.push(`Required skills: ${next.skills.map(skill => safeStateText(skill, 80)).join(' -> ')}.`);
|
|
271
|
+
if (Array.isArray(next.operations) && next.operations.length) parts.push(`Required operations: ${next.operations.map(operation => safeStateText(operation, 80)).join(' -> ')}.`);
|
|
272
|
+
if (next.command) parts.push(`Run: ${safeStateText(next.command, 240)}`);
|
|
247
273
|
if (next.target_phase) parts.push(`Target phase: ${safeStateText(next.target_phase, 80)}.`);
|
|
248
274
|
if (next.status === 'needs_user' || state.status === 'needs_decision' || state.status === 'not_verified') {
|
|
249
275
|
parts.push('Do not deliver as complete until the user decision or accepted gap is recorded.');
|
|
@@ -265,6 +291,52 @@ function contextMapSteering(root) {
|
|
|
265
291
|
].join(' ');
|
|
266
292
|
}
|
|
267
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Compose the SessionStart advisory line for installed-skill drift (#439). Fires only inside a
|
|
296
|
+
* kit-bearing checkout (repo has `kits/` AND a built `dist/claude-code/.claude/skills` bundle) —
|
|
297
|
+
* never triggers a build itself, staying read-only and cheap on every SessionStart. Returns ''
|
|
298
|
+
* (never throws past this function) in every other case: not kit-bearing, no manifest yet, no
|
|
299
|
+
* drift found, or any internal error.
|
|
300
|
+
*
|
|
301
|
+
* Reuses the SINGLE shared classification choke point, `scripts/hooks/lib/skill-drift.js`'s
|
|
302
|
+
* `compareSkillDrift()` — this function must never hand-roll its own drift comparison (same
|
|
303
|
+
* convention the CLI check and the manifest writer follow, see #439 plan).
|
|
304
|
+
*
|
|
305
|
+
* @param {string} root Repository root
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
function skillDriftSteering(root) {
|
|
309
|
+
try {
|
|
310
|
+
if (!fs.existsSync(path.join(root, 'kits'))) return '';
|
|
311
|
+
if (!fs.existsSync(path.join(root, 'dist', 'claude-code', '.claude', 'skills'))) return '';
|
|
312
|
+
|
|
313
|
+
const { loadManifest, compareSkillDrift, resolveClaudeGlobalSkillsDir } = require('./lib/skill-drift');
|
|
314
|
+
|
|
315
|
+
const destDir = resolveClaudeGlobalSkillsDir(process.env);
|
|
316
|
+
// Sibling layout: <destDir>/.flow-agents/skills-manifest.json — mirrors
|
|
317
|
+
// src/lib/local-artifact-root.ts's durableFlowAgentsRoot(destDir)/skillsManifestPath exactly
|
|
318
|
+
// (durableFlowAgentsRoot(cwd) resolves to `${cwd}/.flow-agents`, no parent traversal).
|
|
319
|
+
const manifestPath = path.join(destDir, '.flow-agents', 'skills-manifest.json');
|
|
320
|
+
const manifest = loadManifest(manifestPath);
|
|
321
|
+
|
|
322
|
+
const installedDir = path.join(destDir, 'skills');
|
|
323
|
+
const kitSourceDir = path.join(root, 'dist', 'claude-code', '.claude', 'skills');
|
|
324
|
+
const report = compareSkillDrift({ installedDir, kitSourceDir, manifest });
|
|
325
|
+
if (!report.hasDrift) return '';
|
|
326
|
+
|
|
327
|
+
const { kitUpdated, userModified, unbaselined, missingInstall, kitRemoved } = report.summary;
|
|
328
|
+
// kit_removed (#439 review fix) folds into staleCount alongside the other non-in_sync
|
|
329
|
+
// states — this advisory line only ever surfaces kit-updated/user-modified counts by name
|
|
330
|
+
// (see the template string below), so kit_removed does not get its own named count here; it
|
|
331
|
+
// is still reflected in the total. Full per-state detail is always available via
|
|
332
|
+
// `flow-agents skill-drift-check`.
|
|
333
|
+
const staleCount = kitUpdated + userModified + unbaselined + missingInstall + kitRemoved;
|
|
334
|
+
return `[SKILL DRIFT] ${staleCount} installed Claude Code skill file(s) are stale vs this kit (${kitUpdated} kit-updated, ${userModified} user-modified). Run \`flow-agents init --runtime claude-code --global\` to refresh (user-modified files are reported, not overwritten). See \`flow-agents skill-drift-check\` for details.`;
|
|
335
|
+
} catch {
|
|
336
|
+
return '';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
268
340
|
function promptText(input) {
|
|
269
341
|
const candidates = [
|
|
270
342
|
input && input.prompt,
|
|
@@ -279,7 +351,7 @@ function promptText(input) {
|
|
|
279
351
|
return '';
|
|
280
352
|
}
|
|
281
353
|
|
|
282
|
-
function
|
|
354
|
+
function looksLikeImplementationWork(text) {
|
|
283
355
|
const normalized = safeStateText(text, 2000).toLowerCase();
|
|
284
356
|
if (!normalized) return false;
|
|
285
357
|
const readOnlyIntent = /\b(read[- ]only|review[- ]only|no (source |code |file )?(changes?|edits?|modifications?)|do not (modify|edit|change|write|fix|implement|touch)|don't (modify|edit|change|write|fix|implement|touch)|dont (modify|edit|change|write|fix|implement|touch))\b/.test(normalized);
|
|
@@ -298,17 +370,29 @@ function looksLikeBuilderWork(text) {
|
|
|
298
370
|
return hasWorkVerb || (hasWorkObject && hasRequestLanguage);
|
|
299
371
|
}
|
|
300
372
|
|
|
301
|
-
function
|
|
302
|
-
|
|
303
|
-
return
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
]
|
|
373
|
+
function looksLikeKnowledgeWork(text) {
|
|
374
|
+
const normalized = safeStateText(text, 2000).toLowerCase();
|
|
375
|
+
if (!normalized) return false;
|
|
376
|
+
return /\b(remember|capture|save|file|bookmark)\s+(this|that|the|my|our)\b/.test(normalized) ||
|
|
377
|
+
/\bcapture\s+(this\s+)?(decision|learning|lesson|note|context)\b/.test(normalized) ||
|
|
378
|
+
/\bwhat did we learn about\b/.test(normalized);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function kitWorkflowSteering(input, root) {
|
|
382
|
+
const prompt = promptText(input);
|
|
383
|
+
const categories = [];
|
|
384
|
+
if (looksLikeImplementationWork(prompt)) categories.push('implementation-work-detected');
|
|
385
|
+
if (looksLikeKnowledgeWork(prompt)) categories.push('knowledge-capture-detected');
|
|
386
|
+
if (!categories.length) return '';
|
|
387
|
+
try {
|
|
388
|
+
return categories
|
|
389
|
+
.flatMap(category => workflowTriggersFor(root, category))
|
|
390
|
+
.map(trigger => trigger.steering)
|
|
391
|
+
.filter(Boolean)
|
|
392
|
+
.join('\n');
|
|
393
|
+
} catch {
|
|
394
|
+
return '';
|
|
395
|
+
}
|
|
312
396
|
}
|
|
313
397
|
|
|
314
398
|
/**
|
|
@@ -341,6 +425,13 @@ function resumeSteering(root, current) {
|
|
|
341
425
|
// Full next action (240-char display path, not the 80-char normalization)
|
|
342
426
|
const nextSummary = next.summary ? safeStateText(next.summary, 240) : 'none';
|
|
343
427
|
lines.push(`Next action: ${nextSummary}`);
|
|
428
|
+
if (Array.isArray(next.skills) && next.skills.length) {
|
|
429
|
+
lines.push(`Required skills: ${next.skills.map(skill => safeStateText(skill, 80)).join(' -> ')}`);
|
|
430
|
+
}
|
|
431
|
+
if (Array.isArray(next.operations) && next.operations.length) {
|
|
432
|
+
lines.push(`Required operations: ${next.operations.map(operation => safeStateText(operation, 80)).join(' -> ')}`);
|
|
433
|
+
}
|
|
434
|
+
if (next.command) lines.push(`Run: ${safeStateText(next.command, 240)}`);
|
|
344
435
|
|
|
345
436
|
// Plan artifact path
|
|
346
437
|
let planPath = 'not found';
|
|
@@ -505,9 +596,9 @@ function run(rawInput) {
|
|
|
505
596
|
}
|
|
506
597
|
|
|
507
598
|
if (event === 'UserPromptSubmit') {
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
hints.push(
|
|
599
|
+
const kitHint = kitWorkflowSteering(input, root);
|
|
600
|
+
if (kitHint) {
|
|
601
|
+
hints.push(kitHint);
|
|
511
602
|
const contextHint = contextMapSteering(root);
|
|
512
603
|
if (contextHint) hints.push(contextHint);
|
|
513
604
|
}
|
|
@@ -528,6 +619,14 @@ function run(rawInput) {
|
|
|
528
619
|
if (supersessionHint) hints.push(supersessionHint);
|
|
529
620
|
}
|
|
530
621
|
|
|
622
|
+
// SessionStart only, unconditional of `current` (#439): the installed-skill drift advisory
|
|
623
|
+
// fires on every SessionStart inside a kit-bearing checkout, independent of whether an active
|
|
624
|
+
// workflow session exists — do NOT fold this into the `current`-gated block above.
|
|
625
|
+
if (event === 'SessionStart') {
|
|
626
|
+
const driftHint = skillDriftSteering(root);
|
|
627
|
+
if (driftHint) hints.push(driftHint);
|
|
628
|
+
}
|
|
629
|
+
|
|
531
630
|
if (shouldAppendWorkflowContext) {
|
|
532
631
|
const stateHint = stateSteering(root);
|
|
533
632
|
if (stateHint) hints.push(stateHint);
|
|
@@ -558,6 +657,7 @@ module.exports = {
|
|
|
558
657
|
stateSteering,
|
|
559
658
|
critiqueSteering,
|
|
560
659
|
contextMapSteering,
|
|
660
|
+
skillDriftSteering,
|
|
561
661
|
latestWorkflowState,
|
|
562
662
|
findRepoRoot,
|
|
563
663
|
safeStateText,
|
|
@@ -565,6 +665,6 @@ module.exports = {
|
|
|
565
665
|
resumeSteering,
|
|
566
666
|
supersessionSteering,
|
|
567
667
|
promptText,
|
|
568
|
-
|
|
569
|
-
|
|
668
|
+
looksLikeImplementationWork,
|
|
669
|
+
kitWorkflowSteering,
|
|
570
670
|
};
|
|
@@ -51,6 +51,41 @@ while [[ $# -gt 0 ]]; do
|
|
|
51
51
|
esac
|
|
52
52
|
done
|
|
53
53
|
REAL_CODEX_HOME="${CODEX_REAL_HOME:-$HOME/.codex}"
|
|
54
|
+
|
|
55
|
+
canonicalize_path() {
|
|
56
|
+
node - "$1" <<'NODE'
|
|
57
|
+
const fs = require("node:fs");
|
|
58
|
+
const path = require("node:path");
|
|
59
|
+
let current = path.resolve(process.argv[2]);
|
|
60
|
+
const missing = [];
|
|
61
|
+
while (!fs.existsSync(current)) {
|
|
62
|
+
missing.unshift(path.basename(current));
|
|
63
|
+
const parent = path.dirname(current);
|
|
64
|
+
if (parent === current) break;
|
|
65
|
+
current = parent;
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write(path.resolve(fs.realpathSync(current), ...missing));
|
|
68
|
+
NODE
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
[[ ! -L "$DEST" ]] || { echo "install-codex-home.sh: refusing symlink destination root: $DEST" >&2; exit 1; }
|
|
72
|
+
ROOT_REAL="$(canonicalize_path "$ROOT_DIR")"
|
|
73
|
+
BUNDLE_SOURCE_REAL="$(canonicalize_path "$ROOT_DIR/dist/codex")"
|
|
74
|
+
DEST_REAL="$(canonicalize_path "$DEST")"
|
|
75
|
+
REAL_CODEX_HOME_REAL="$(canonicalize_path "$REAL_CODEX_HOME")"
|
|
76
|
+
case "$DEST_REAL/" in
|
|
77
|
+
"$ROOT_REAL/"*|"$BUNDLE_SOURCE_REAL/"*)
|
|
78
|
+
echo "install-codex-home.sh: destination overlaps Flow Agents source: $DEST_REAL" >&2
|
|
79
|
+
exit 1
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
case "$ROOT_REAL/" in
|
|
83
|
+
"$DEST_REAL/"*)
|
|
84
|
+
echo "install-codex-home.sh: Flow Agents source overlaps destination: $DEST_REAL" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
|
|
54
89
|
if command -v npm >/dev/null 2>&1; then
|
|
55
90
|
(cd "$ROOT_DIR" && FLOW_AGENTS_EXPORT_DIAGNOSTICS=0 npm run build:bundles --silent >/dev/null)
|
|
56
91
|
else
|
|
@@ -59,7 +94,8 @@ else
|
|
|
59
94
|
fi
|
|
60
95
|
|
|
61
96
|
mkdir -p "$DEST"
|
|
62
|
-
|
|
97
|
+
DEST="$(cd "$DEST" && pwd -P)"
|
|
98
|
+
DEST_REAL="$DEST"
|
|
63
99
|
|
|
64
100
|
assert_safe_dest_path() {
|
|
65
101
|
local rel="$1"
|
|
@@ -95,10 +131,17 @@ if [[ -f "$DEST/hooks.json" ]]; then
|
|
|
95
131
|
cp "$DEST/hooks.json" "$FA_USER_HOOKS_STASH"
|
|
96
132
|
fi
|
|
97
133
|
|
|
98
|
-
# A real Codex home can contain user-owned
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
134
|
+
# A real Codex home can contain user-owned files in every shared directory.
|
|
135
|
+
# Prepare an exact Flow Agents overlay, then synchronize only files recorded in
|
|
136
|
+
# the ownership manifest. The synchronizer never uses directory-wide --delete:
|
|
137
|
+
# it removes only unchanged files from its prior manifest and refuses collisions.
|
|
138
|
+
FA_OWNED_OVERLAY="$(mktemp -d /tmp/fa-codex-overlay.XXXXXX)"
|
|
139
|
+
cleanup_install_temps() {
|
|
140
|
+
rm -rf "$FA_OWNED_OVERLAY"
|
|
141
|
+
[[ -z "${FA_USER_HOOKS_STASH:-}" ]] || rm -f "$FA_USER_HOOKS_STASH"
|
|
142
|
+
}
|
|
143
|
+
trap cleanup_install_temps EXIT
|
|
144
|
+
|
|
102
145
|
for managed_dir in \
|
|
103
146
|
.flow-agents \
|
|
104
147
|
agent-cards \
|
|
@@ -115,52 +158,65 @@ for managed_dir in \
|
|
|
115
158
|
do
|
|
116
159
|
if [[ -d "$ROOT_DIR/dist/codex/$managed_dir" ]]; then
|
|
117
160
|
assert_safe_dest_path "$managed_dir"
|
|
118
|
-
mkdir -p "$
|
|
119
|
-
rsync -a
|
|
161
|
+
mkdir -p "$FA_OWNED_OVERLAY/$managed_dir"
|
|
162
|
+
rsync -a "$ROOT_DIR/dist/codex/$managed_dir/" "$FA_OWNED_OVERLAY/$managed_dir/"
|
|
120
163
|
fi
|
|
121
164
|
done
|
|
122
165
|
|
|
123
166
|
# Skills are user-extensible in a real Codex home. Merge both bundle skill
|
|
124
167
|
# layers into the flattened destination without deleting user-owned skills.
|
|
125
168
|
if [[ -d "$ROOT_DIR/dist/codex/skills" ]]; then
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
rsync -a "$ROOT_DIR/dist/codex/skills/" "$DEST/skills/"
|
|
169
|
+
mkdir -p "$FA_OWNED_OVERLAY/skills"
|
|
170
|
+
rsync -a "$ROOT_DIR/dist/codex/skills/" "$FA_OWNED_OVERLAY/skills/"
|
|
129
171
|
fi
|
|
130
172
|
|
|
131
173
|
if [[ -d "$ROOT_DIR/dist/codex/.codex/skills" ]]; then
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
rsync -a "$ROOT_DIR/dist/codex/.codex/skills/" "$DEST/skills/"
|
|
174
|
+
mkdir -p "$FA_OWNED_OVERLAY/skills"
|
|
175
|
+
rsync -a "$ROOT_DIR/dist/codex/.codex/skills/" "$FA_OWNED_OVERLAY/skills/"
|
|
135
176
|
fi
|
|
136
177
|
|
|
137
178
|
if [[ -d "$ROOT_DIR/dist/codex/kits" ]]; then
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
rsync -a --delete --exclude 'local/' "$ROOT_DIR/dist/codex/kits/" "$DEST/kits/"
|
|
179
|
+
mkdir -p "$FA_OWNED_OVERLAY/kits"
|
|
180
|
+
rsync -a --exclude 'local/' "$ROOT_DIR/dist/codex/kits/" "$FA_OWNED_OVERLAY/kits/"
|
|
141
181
|
fi
|
|
142
182
|
|
|
143
183
|
# Agents are user-extensible in a real Codex home. Merge generated agents
|
|
144
184
|
# without deleting user-owned agents.
|
|
145
185
|
if [[ -d "$ROOT_DIR/dist/codex/.codex/agents" ]]; then
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
rsync -a "$ROOT_DIR/dist/codex/.codex/agents/" "$DEST/agents/"
|
|
186
|
+
mkdir -p "$FA_OWNED_OVERLAY/agents"
|
|
187
|
+
rsync -a "$ROOT_DIR/dist/codex/.codex/agents/" "$FA_OWNED_OVERLAY/agents/"
|
|
149
188
|
fi
|
|
150
189
|
|
|
151
190
|
for bundle_file in README.md console.telemetry.json install.sh; do
|
|
152
191
|
if [[ -f "$ROOT_DIR/dist/codex/$bundle_file" ]]; then
|
|
153
|
-
|
|
154
|
-
cp "$ROOT_DIR/dist/codex/$bundle_file" "$DEST/$bundle_file"
|
|
192
|
+
cp "$ROOT_DIR/dist/codex/$bundle_file" "$FA_OWNED_OVERLAY/$bundle_file"
|
|
155
193
|
fi
|
|
156
194
|
done
|
|
157
195
|
|
|
196
|
+
assert_safe_dest_path ".flow-agents/codex-install-manifest.json"
|
|
197
|
+
node "$ROOT_DIR/scripts/install-owned-files.js" \
|
|
198
|
+
"$FA_OWNED_OVERLAY" "$DEST" ".flow-agents/codex-install-manifest.json"
|
|
199
|
+
|
|
200
|
+
atomic_copy() {
|
|
201
|
+
local source="$1"
|
|
202
|
+
local rel="$2"
|
|
203
|
+
assert_safe_dest_path "$rel"
|
|
204
|
+
local target="$DEST/$rel"
|
|
205
|
+
if [[ -e "$target" && ! -f "$target" ]]; then
|
|
206
|
+
echo "install-codex-home.sh: refusing to replace non-file: $target" >&2
|
|
207
|
+
exit 1
|
|
208
|
+
fi
|
|
209
|
+
local temp
|
|
210
|
+
temp="$(mktemp "$(dirname "$target")/.flow-agents-install.XXXXXX")"
|
|
211
|
+
cp "$source" "$temp"
|
|
212
|
+
mv -f "$temp" "$target"
|
|
213
|
+
}
|
|
214
|
+
|
|
158
215
|
# Root Codex config/profiles and AGENTS.md may be user-owned in ~/.codex.
|
|
159
216
|
# Seed Flow Agents defaults only when absent.
|
|
160
217
|
for seed_file in AGENTS.md; do
|
|
161
218
|
if [[ -f "$ROOT_DIR/dist/codex/$seed_file" && ! -e "$DEST/$seed_file" ]]; then
|
|
162
|
-
|
|
163
|
-
cp "$ROOT_DIR/dist/codex/$seed_file" "$DEST/$seed_file"
|
|
219
|
+
atomic_copy "$ROOT_DIR/dist/codex/$seed_file" "$seed_file"
|
|
164
220
|
fi
|
|
165
221
|
done
|
|
166
222
|
generated_profile_files=()
|
|
@@ -173,8 +229,7 @@ for seed_source in "$ROOT_DIR/dist/codex/.codex/config.toml" "$ROOT_DIR"/dist/co
|
|
|
173
229
|
profile_names+=("${seed_file%.config.toml}")
|
|
174
230
|
fi
|
|
175
231
|
if [[ ! -e "$DEST/$seed_file" ]] || grep -q 'Generated from packaging/manifest.json' "$DEST/$seed_file"; then
|
|
176
|
-
|
|
177
|
-
cp "$seed_source" "$DEST/$seed_file"
|
|
232
|
+
atomic_copy "$seed_source" "$seed_file"
|
|
178
233
|
fi
|
|
179
234
|
done
|
|
180
235
|
for profile_path in "$DEST"/*.config.toml; do
|
|
@@ -195,9 +250,8 @@ done
|
|
|
195
250
|
rmdir "$DEST/.codex" 2>/dev/null || true
|
|
196
251
|
|
|
197
252
|
for auth_file in auth.json version.json installation_id models_cache.json; do
|
|
198
|
-
if [[ "$
|
|
199
|
-
|
|
200
|
-
cp "$REAL_CODEX_HOME/$auth_file" "$DEST/$auth_file"
|
|
253
|
+
if [[ "$REAL_CODEX_HOME_REAL" != "$DEST_REAL" && -f "$REAL_CODEX_HOME_REAL/$auth_file" ]]; then
|
|
254
|
+
atomic_copy "$REAL_CODEX_HOME_REAL/$auth_file" "$auth_file"
|
|
201
255
|
fi
|
|
202
256
|
done
|
|
203
257
|
|
|
@@ -212,26 +266,22 @@ chmod 700 "$DEST" 2>/dev/null || true
|
|
|
212
266
|
FA_VERSION="$(node -p "require('$ROOT_DIR/package.json').version" 2>/dev/null || echo unknown)"
|
|
213
267
|
FA_MANAGED_HOOKS="$ROOT_DIR/dist/codex/.codex/hooks.json"
|
|
214
268
|
if command -v node >/dev/null 2>&1 && [[ -f "$FA_MANAGED_HOOKS" ]]; then
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
--managed-hooks "$FA_MANAGED_HOOKS" \
|
|
220
|
-
--version "$FA_VERSION" \
|
|
221
|
-
--install-record "$DEST/.flow-agents/install.json" \
|
|
222
|
-
--runtime "codex"
|
|
223
|
-
# Move the merged result into the destination.
|
|
224
|
-
cp "$FA_USER_HOOKS_STASH" "$DEST/hooks.json"
|
|
225
|
-
rm -f "$FA_USER_HOOKS_STASH"
|
|
226
|
-
else
|
|
227
|
-
# No prior user hooks: just write the version stamp (FA hooks are already correct from rsync).
|
|
228
|
-
node "$ROOT_DIR/scripts/install-merge.js" \
|
|
229
|
-
--config "$DEST/hooks.json" \
|
|
230
|
-
--managed-hooks "$FA_MANAGED_HOOKS" \
|
|
231
|
-
--version "$FA_VERSION" \
|
|
232
|
-
--install-record "$DEST/.flow-agents/install.json" \
|
|
233
|
-
--runtime "codex"
|
|
269
|
+
FA_HOOKS_WORK="${FA_USER_HOOKS_STASH:-}"
|
|
270
|
+
if [[ -z "$FA_HOOKS_WORK" ]]; then
|
|
271
|
+
FA_HOOKS_WORK="$(mktemp /tmp/fa-hooks-work.XXXXXX.json)"
|
|
272
|
+
printf '{"hooks":{}}\n' > "$FA_HOOKS_WORK"
|
|
234
273
|
fi
|
|
274
|
+
FA_INSTALL_RECORD_WORK="$(mktemp /tmp/fa-install-record.XXXXXX.json)"
|
|
275
|
+
node "$ROOT_DIR/scripts/install-merge.js" \
|
|
276
|
+
--config "$FA_HOOKS_WORK" \
|
|
277
|
+
--managed-hooks "$FA_MANAGED_HOOKS" \
|
|
278
|
+
--version "$FA_VERSION" \
|
|
279
|
+
--install-record "$FA_INSTALL_RECORD_WORK" \
|
|
280
|
+
--runtime "codex"
|
|
281
|
+
atomic_copy "$FA_HOOKS_WORK" "hooks.json"
|
|
282
|
+
atomic_copy "$FA_INSTALL_RECORD_WORK" ".flow-agents/install.json"
|
|
283
|
+
rm -f "$FA_HOOKS_WORK" "$FA_INSTALL_RECORD_WORK"
|
|
284
|
+
FA_USER_HOOKS_STASH=""
|
|
235
285
|
fi
|
|
236
286
|
|
|
237
287
|
if [[ ${#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
|
package/scripts/install-merge.js
CHANGED
|
@@ -23,10 +23,13 @@
|
|
|
23
23
|
* flow-agents marker (the COLLISION_MARKER strings from init.ts).
|
|
24
24
|
* (c) APPEND the current managed hook groups from the bundle.
|
|
25
25
|
* (d) Preserve ALL other keys (permissions, statusLine, user hooks, auth).
|
|
26
|
+
* Managed non-hook values are added only when absent or when replacing
|
|
27
|
+
* an existing Flow Agents-owned value. Conflicting user-owned values win
|
|
28
|
+
* and are reported to stderr.
|
|
26
29
|
* (e) Atomic write (write tmp + rename).
|
|
27
30
|
* (f) Write/update .flow-agents/install.json version stamp.
|
|
28
31
|
*
|
|
29
|
-
* Export: mergeSettings(existing, managed) — pure, testable.
|
|
32
|
+
* Export: mergeSettings(existing, managed, options) — pure, testable.
|
|
30
33
|
* The managed ownership region is identified purely by statusMessage markers
|
|
31
34
|
* (cross-runtime, no top-level key needed in settings.json).
|
|
32
35
|
*/
|
|
@@ -47,6 +50,33 @@ const FA_MARKERS = [
|
|
|
47
50
|
"Capturing Flow Agents command evidence",
|
|
48
51
|
];
|
|
49
52
|
|
|
53
|
+
const FA_OWNERSHIP_MARKERS = [
|
|
54
|
+
...FA_MARKERS,
|
|
55
|
+
"flow-agents",
|
|
56
|
+
"Flow Agents",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function stableJson(value) {
|
|
60
|
+
if (value === undefined) return "undefined";
|
|
61
|
+
return JSON.stringify(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function valuesEqual(a, b) {
|
|
65
|
+
return stableJson(a) === stableJson(b);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function valueContainsManagedMarker(value) {
|
|
69
|
+
return FA_OWNERSHIP_MARKERS.some((marker) => stableJson(value).includes(marker));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function conflict(path, existingValue, managedValue) {
|
|
73
|
+
return { path, existingValue, managedValue };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function emitConflict(onConflict, item) {
|
|
77
|
+
if (typeof onConflict === "function") onConflict(item);
|
|
78
|
+
}
|
|
79
|
+
|
|
50
80
|
/**
|
|
51
81
|
* Returns true if a hook-group entry is owned by flow-agents.
|
|
52
82
|
* A hook-group in Claude Code settings looks like:
|
|
@@ -95,16 +125,28 @@ function mergeArrayUnion(a, b) {
|
|
|
95
125
|
* @param {unknown} existingPerms @param {unknown} managedPerms
|
|
96
126
|
* @returns {Record<string, unknown>}
|
|
97
127
|
*/
|
|
98
|
-
function mergePermissions(existingPerms, managedPerms) {
|
|
128
|
+
function mergePermissions(existingPerms, managedPerms, options = {}) {
|
|
99
129
|
const e = existingPerms && typeof existingPerms === "object" ? existingPerms : {};
|
|
100
130
|
const m = managedPerms && typeof managedPerms === "object" ? managedPerms : {};
|
|
101
|
-
const out = Object.assign({}, e
|
|
131
|
+
const out = Object.assign({}, e);
|
|
102
132
|
for (const listKey of ["allow", "deny", "ask"]) {
|
|
103
133
|
if (Array.isArray(e[listKey]) || Array.isArray(m[listKey])) {
|
|
104
134
|
out[listKey] = mergeArrayUnion(e[listKey], m[listKey]);
|
|
105
135
|
}
|
|
106
136
|
}
|
|
107
|
-
|
|
137
|
+
for (const [key, value] of Object.entries(m)) {
|
|
138
|
+
if (["allow", "deny", "ask"].includes(key)) continue;
|
|
139
|
+
if (!(key in e)) {
|
|
140
|
+
out[key] = value;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (valuesEqual(e[key], value) || valueContainsManagedMarker(e[key])) {
|
|
144
|
+
out[key] = value;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
out[key] = e[key];
|
|
148
|
+
emitConflict(options.onConflict, conflict(`permissions.${key}`, e[key], value));
|
|
149
|
+
}
|
|
108
150
|
return out;
|
|
109
151
|
}
|
|
110
152
|
|
|
@@ -117,14 +159,17 @@ function mergePermissions(existingPerms, managedPerms) {
|
|
|
117
159
|
*
|
|
118
160
|
* Returns a new object with:
|
|
119
161
|
* - All keys from `existing` preserved (permissions, statusLine, auth, etc.)
|
|
120
|
-
* -
|
|
162
|
+
* - Managed keys added when absent, or updated only when the existing value
|
|
163
|
+
* is already Flow Agents-owned.
|
|
164
|
+
* - User-owned managed-key conflicts preserved from `existing` and surfaced
|
|
165
|
+
* through `options.onConflict`.
|
|
121
166
|
* - For the `hooks` key: user-owned hook groups (non-FA) survive; FA groups are
|
|
122
167
|
* replaced with the current managed set from `managed`.
|
|
123
168
|
*
|
|
124
169
|
* Strategy:
|
|
125
170
|
* 1. Start with a shallow copy of `existing` (preserves all user keys).
|
|
126
|
-
* 2.
|
|
127
|
-
*
|
|
171
|
+
* 2. Add or update Flow Agents-owned non-hooks keys from `managed`; preserve
|
|
172
|
+
* user-owned conflicting values and report conflicts.
|
|
128
173
|
* 3. For `hooks`: iterate each event key from both existing and managed;
|
|
129
174
|
* keep user (non-FA) groups from existing, append the current FA groups
|
|
130
175
|
* from managed.
|
|
@@ -133,19 +178,26 @@ function mergePermissions(existingPerms, managedPerms) {
|
|
|
133
178
|
* @param {Record<string, unknown>} managed
|
|
134
179
|
* @returns {Record<string, unknown>}
|
|
135
180
|
*/
|
|
136
|
-
function mergeSettings(existing, managed) {
|
|
181
|
+
function mergeSettings(existing, managed, options = {}) {
|
|
137
182
|
// 1. Start with all existing keys (preserves user-owned data).
|
|
138
183
|
const result = Object.assign({}, existing);
|
|
139
184
|
|
|
140
|
-
// 2.
|
|
141
|
-
//
|
|
142
|
-
//
|
|
185
|
+
// 2. Add non-hooks keys from managed without overwriting user-owned values.
|
|
186
|
+
// If a key is absent, add the managed default. If the existing value is
|
|
187
|
+
// already Flow Agents-owned (for example the generated statusLine command),
|
|
188
|
+
// replace it with the current managed value. Otherwise preserve the user's
|
|
189
|
+
// value and surface a conflict so the user can decide.
|
|
143
190
|
for (const [key, value] of Object.entries(managed)) {
|
|
144
191
|
if (key === "hooks") continue;
|
|
145
192
|
if (key === "permissions") {
|
|
146
|
-
result.permissions = mergePermissions(existing.permissions, value);
|
|
147
|
-
} else {
|
|
193
|
+
result.permissions = mergePermissions(existing.permissions, value, options);
|
|
194
|
+
} else if (!(key in existing)) {
|
|
148
195
|
result[key] = value;
|
|
196
|
+
} else if (valuesEqual(existing[key], value) || valueContainsManagedMarker(existing[key])) {
|
|
197
|
+
result[key] = value;
|
|
198
|
+
} else {
|
|
199
|
+
result[key] = existing[key];
|
|
200
|
+
emitConflict(options.onConflict, conflict(key, existing[key], value));
|
|
149
201
|
}
|
|
150
202
|
}
|
|
151
203
|
|
|
@@ -267,7 +319,13 @@ function runMerge({ configPath, managedHooksPath, version, installRecordPath, ru
|
|
|
267
319
|
}
|
|
268
320
|
|
|
269
321
|
// (b) + (c) + (d) Merge.
|
|
270
|
-
const
|
|
322
|
+
const conflicts = [];
|
|
323
|
+
const merged = mergeSettings(existing, managed, { onConflict: (item) => conflicts.push(item) });
|
|
324
|
+
for (const item of conflicts) {
|
|
325
|
+
process.stderr.write(
|
|
326
|
+
`install-merge: conflict: preserving existing setting '${item.path}' and not applying Flow Agents managed value\n`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
271
329
|
|
|
272
330
|
// (e) Atomic write.
|
|
273
331
|
atomicWriteJson(configPath, merged);
|