@kontourai/flow-agents 3.2.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 +173 -0
- package/CHANGELOG.md +54 -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/assignment-provider.js +10 -1
- 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-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +310 -1
- package/build/src/cli/workflow-sidecar.js +1914 -126
- 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 +30 -3
- package/context/contracts/artifact-contract.md +16 -2
- 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 +194 -22
- 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/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +5 -3
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +6 -2
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +23 -9
- package/docs/integrations/flow-agents-console.md +275 -0
- package/docs/integrations/index.md +4 -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-artifact-lifecycle.md +38 -1
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +2 -0
- package/evals/ci/run-baseline.sh +78 -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/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -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_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -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 +389 -2
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- 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_takeover_protocol.sh +340 -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 +121 -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_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- 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 +37 -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/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +115 -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 +33 -2
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +113 -7
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +19 -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 +214 -253
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/actor-identity.js +82 -0
- 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 +194 -22
- 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/lib/reconcile-shape.js +381 -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/assignment-provider.ts +12 -1
- 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-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +1980 -119
- 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 +29 -3
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* skill-drift.js — shared pure-CJS manifest/drift-comparison library for installed Claude Code
|
|
4
|
+
* skill files (kontourai/flow-agents#439, slice 1).
|
|
5
|
+
*
|
|
6
|
+
* Zero external dependencies (only Node core: fs, path, crypto, os). Consumed by:
|
|
7
|
+
* - src/cli/init.ts (compiled ESM, via createRequire — writes the manifest at
|
|
8
|
+
* `init --global` sync time, Wave 2)
|
|
9
|
+
* - src/cli/skill-drift-check.ts (compiled ESM, via createRequire — standalone read-only
|
|
10
|
+
* CLI check, Wave 2)
|
|
11
|
+
* - scripts/hooks/workflow-steering.js (CJS, direct require — SessionStart advisory, Wave 2)
|
|
12
|
+
*
|
|
13
|
+
* Purpose (issue #439): this is the SINGLE choke point for classifying installed-skill drift so
|
|
14
|
+
* the manifest writer, the CLI check, and the SessionStart advisory can never disagree with one
|
|
15
|
+
* another about what "in sync" / "stale" / "user-modified" means for a given file. Every consumer
|
|
16
|
+
* calls `compareSkillDrift()` instead of hand-rolling its own comparison, mirroring the same
|
|
17
|
+
* "one shared CJS module, multiple TS/hook consumers" convention already established by
|
|
18
|
+
* `current-pointer.js` (#291) and `actor-identity.js` for this repo's TS/CJS boundary.
|
|
19
|
+
*
|
|
20
|
+
* Hashing convention: `hashFile()` returns `"sha256:<hex>"` — the SAME string prefix convention
|
|
21
|
+
* `src/cli/kit.ts`'s `contentHash`/`kitContentHash` already established for Flow Kit installs.
|
|
22
|
+
* This module does not invent a second hash format (ADR 0008/0010 consume-never-fork).
|
|
23
|
+
*
|
|
24
|
+
* File-walk convention: `walkFilesSorted()`/`relPathPosix()` re-implement the SAME
|
|
25
|
+
* sorted-recursive-`readdirSync` traversal and POSIX-joined relative-path normalization as
|
|
26
|
+
* `src/lib/fs.ts`'s `walkFiles`/`relPath` — byte-identical relative-path strings for the same
|
|
27
|
+
* directory tree. This file cannot `import` that TS module directly (it must stay pure CJS so
|
|
28
|
+
* `workflow-steering.js` can `require()` it directly without a compiled-ESM detour), so it is an
|
|
29
|
+
* intentional, minimal, SAME-SHAPE re-implementation across the CJS boundary, not a fork of
|
|
30
|
+
* behavior — this must not diverge from `walkFiles`/`relPath` at review time.
|
|
31
|
+
*
|
|
32
|
+
* Exports:
|
|
33
|
+
* hashFile(absPath) → "sha256:<hex>" string
|
|
34
|
+
* buildManifest({ skillsSourceDir, runtime }) → manifest object (see below)
|
|
35
|
+
* loadManifest(manifestPath) → parsed manifest object, or null if
|
|
36
|
+
* absent/unreadable (never throws)
|
|
37
|
+
* writeManifestAtomic(manifestPath, manifest) → void (tmp-write-then-rename, same
|
|
38
|
+
* idiom `src/cli/init.ts` uses for
|
|
39
|
+
* `install.json`)
|
|
40
|
+
* compareSkillDrift({ installedDir, kitSourceDir, manifest }) → drift report (see below)
|
|
41
|
+
* resolveClaudeGlobalSkillsDir(env = process.env) → string (the global claude-code dest
|
|
42
|
+
* dir, e.g. `~/.claude`, honoring
|
|
43
|
+
* FLOW_AGENTS_USER_CLAUDE_SETTINGS for
|
|
44
|
+
* test isolation — same resolution
|
|
45
|
+
* `src/cli/init.ts`'s `globalDest`
|
|
46
|
+
* already implements for "claude-code")
|
|
47
|
+
*
|
|
48
|
+
* Manifest shape:
|
|
49
|
+
* {
|
|
50
|
+
* schema_version: "1.0",
|
|
51
|
+
* runtime: string,
|
|
52
|
+
* generatedAt: string (ISO 8601),
|
|
53
|
+
* sourceDir: string,
|
|
54
|
+
* files: { "<skill>/<relpath>": "sha256:<hex>", ... }
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* Drift report shape (from compareSkillDrift):
|
|
58
|
+
* {
|
|
59
|
+
* checkedAt: string (ISO 8601),
|
|
60
|
+
* installedDir: string,
|
|
61
|
+
* kitSourceDir: string,
|
|
62
|
+
* manifestFound: boolean,
|
|
63
|
+
* files: [{ path, state, installedHash, kitHash, manifestHash }],
|
|
64
|
+
* summary: { total, inSync, kitUpdated, userModified, unbaselined, missingInstall, kitRemoved },
|
|
65
|
+
* hasDrift: boolean
|
|
66
|
+
* }
|
|
67
|
+
*
|
|
68
|
+
* Classification states (one of exactly six per file; names must stay identical across every
|
|
69
|
+
* consumer — the manifest writer, the CLI check, and the hook advisory never hand-roll their own
|
|
70
|
+
* classification strings):
|
|
71
|
+
* - "in_sync" installedHash === kitHash (installed bytes already match current kit
|
|
72
|
+
* source, regardless of manifest).
|
|
73
|
+
* - "kit_updated" installedHash !== kitHash but installedHash === manifestHash (installed
|
|
74
|
+
* copy matches the last recorded baseline; kit source has since changed).
|
|
75
|
+
* - "user_modified" installedHash !== manifestHash (and not in_sync) — installed bytes were
|
|
76
|
+
* changed locally since the last recorded install. This ALWAYS wins over
|
|
77
|
+
* "kit_updated" when both installedHash !== kitHash and
|
|
78
|
+
* installedHash !== manifestHash are true (never silently reclassified as
|
|
79
|
+
* a kit update).
|
|
80
|
+
* - "unbaselined" no manifest entry exists for this path (manifest missing entirely, or
|
|
81
|
+
* present but lacking this path's key) and installedHash !== kitHash —
|
|
82
|
+
* reported distinctly, never conflated with user_modified/kit_updated.
|
|
83
|
+
* - "missing_install" a kit-source file has no installed counterpart at all (skill never
|
|
84
|
+
* synced locally yet) — reported distinctly, though also resolved by the
|
|
85
|
+
* same refresh command as the other non-in_sync states.
|
|
86
|
+
* - "kit_removed" kitHash === null (the path no longer exists anywhere in the current kit
|
|
87
|
+
* source) but the installed file still matches the last recorded baseline
|
|
88
|
+
* exactly (installedHash === manifestHash). Distinct from "kit_updated":
|
|
89
|
+
* the additive `copyDirMerge`-based refresh can only add/update files, it
|
|
90
|
+
* can never delete, so it cannot "fix" this state — reported with guidance
|
|
91
|
+
* to review/remove the installed file manually. Sub-case precedence:
|
|
92
|
+
* kitHash === null AND installedHash !== manifestHash still classifies as
|
|
93
|
+
* "user_modified" (installed bytes differ from the recorded baseline wins
|
|
94
|
+
* over "kit_removed", same precedence rule as the kit_updated case above).
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
const fs = require('fs');
|
|
98
|
+
const path = require('path');
|
|
99
|
+
const crypto = require('crypto');
|
|
100
|
+
const os = require('os');
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sorted recursive file walk — mirrors `src/lib/fs.ts`'s `walkFiles` exactly (sorted
|
|
104
|
+
* `fs.readdirSync`, depth-first, files only). Returns absolute paths.
|
|
105
|
+
*
|
|
106
|
+
* Uses `readdirSync(dir, { withFileTypes: true })` Dirent type checks — NOT `fs.statSync`, which
|
|
107
|
+
* follows symlinks — so a symlink under the tree (e.g. pointing outside it, or dangling/looped)
|
|
108
|
+
* is neither followed nor hashed, and never throws (ELOOP / ENOENT on a dangling symlink). This
|
|
109
|
+
* is the exact same semantics `copyDirMerge` already uses for install copying
|
|
110
|
+
* (`src/cli/init.ts`): recurse into `entry.isDirectory()`, include `entry.isFile()`, silently
|
|
111
|
+
* skip everything else (symlinks, sockets, etc). Manifest/drift walking and install copying must
|
|
112
|
+
* never disagree about which files exist.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} root
|
|
115
|
+
* @returns {string[]}
|
|
116
|
+
*/
|
|
117
|
+
function walkFilesSorted(root) {
|
|
118
|
+
const out = [];
|
|
119
|
+
if (!fs.existsSync(root)) return out;
|
|
120
|
+
const entries = fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const file = path.join(root, entry.name);
|
|
123
|
+
if (entry.isDirectory()) out.push(...walkFilesSorted(file));
|
|
124
|
+
else if (entry.isFile()) out.push(file);
|
|
125
|
+
// else: symlink, socket, fifo, etc — skip (see doc comment above).
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* POSIX-joined relative path — mirrors `src/lib/fs.ts`'s `relPath` exactly.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} root
|
|
134
|
+
* @param {string} file
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function relPathPosix(root, file) {
|
|
138
|
+
const rel = path.relative(root, file);
|
|
139
|
+
return rel && !rel.startsWith('..') && !path.isAbsolute(rel)
|
|
140
|
+
? rel.split(path.sep).join('/')
|
|
141
|
+
: file.split(path.sep).join('/');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `sha256:<hex>` content hash of a single file — same prefix convention as
|
|
146
|
+
* `src/cli/kit.ts`'s `contentHash`/`kitContentHash`.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} absPath
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
function hashFile(absPath) {
|
|
152
|
+
const hash = crypto.createHash('sha256');
|
|
153
|
+
hash.update(fs.readFileSync(absPath));
|
|
154
|
+
return `sha256:${hash.digest('hex')}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Builds a per-skill-file content-hash manifest by walking `skillsSourceDir` (e.g. a built
|
|
159
|
+
* `dist/claude-code/.claude/skills` bundle directory). Keys are POSIX relative paths under
|
|
160
|
+
* `skillsSourceDir` (e.g. `pull-work/SKILL.md`).
|
|
161
|
+
*
|
|
162
|
+
* @param {{ skillsSourceDir: string, runtime: string }} params
|
|
163
|
+
* @returns {object} manifest (see module doc comment for shape)
|
|
164
|
+
*/
|
|
165
|
+
function buildManifest({ skillsSourceDir, runtime }) {
|
|
166
|
+
const files = {};
|
|
167
|
+
for (const file of walkFilesSorted(skillsSourceDir)) {
|
|
168
|
+
const rel = relPathPosix(skillsSourceDir, file);
|
|
169
|
+
files[rel] = hashFile(file);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
schema_version: '1.0',
|
|
173
|
+
runtime,
|
|
174
|
+
generatedAt: new Date().toISOString(),
|
|
175
|
+
sourceDir: skillsSourceDir,
|
|
176
|
+
files,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Best-effort tolerant manifest read: missing file or corrupt/unparseable content are BOTH
|
|
182
|
+
* treated as "absent" (returns null), never thrown — mirrors the `readCurrentPointer`-style
|
|
183
|
+
* tolerance already established for advisory reads in this repo.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} manifestPath
|
|
186
|
+
* @returns {object|null}
|
|
187
|
+
*/
|
|
188
|
+
function loadManifest(manifestPath) {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Atomic write: tmp-write-then-rename, the SAME idiom `src/cli/init.ts` already uses for
|
|
198
|
+
* `install.json` (`${path}.tmp.${pid}` then `fs.renameSync`).
|
|
199
|
+
*
|
|
200
|
+
* @param {string} manifestPath
|
|
201
|
+
* @param {object} manifest
|
|
202
|
+
* @returns {void}
|
|
203
|
+
*/
|
|
204
|
+
function writeManifestAtomic(manifestPath, manifest) {
|
|
205
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
206
|
+
const tmp = `${manifestPath}.tmp.${process.pid}`;
|
|
207
|
+
fs.writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
208
|
+
fs.renameSync(tmp, manifestPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Hashes a single relative path under `dir`, if it exists there as a file. Returns null if
|
|
213
|
+
* absent (covers both "not installed yet" and "not present in this kit source snapshot").
|
|
214
|
+
*
|
|
215
|
+
* @param {string} dir
|
|
216
|
+
* @param {string} rel
|
|
217
|
+
* @returns {string|null}
|
|
218
|
+
*/
|
|
219
|
+
function hashRelIfPresent(dir, rel) {
|
|
220
|
+
const abs = path.join(dir, rel);
|
|
221
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
|
|
222
|
+
try {
|
|
223
|
+
return hashFile(abs);
|
|
224
|
+
} catch {
|
|
225
|
+
// Race between the existsSync/statSync check above and this read (file deleted or made
|
|
226
|
+
// unreadable in between — ENOENT/EACCES) — treat as "hash unavailable" (null), same as "not
|
|
227
|
+
// present", rather than throwing and crashing the whole drift check over one flaky file.
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Compares installed skill files, the current kit/bundle source, and the last-recorded manifest
|
|
234
|
+
* baseline, classifying every relative path into exactly one of six states. See the module doc
|
|
235
|
+
* comment for the full state semantics and precedence rules.
|
|
236
|
+
*
|
|
237
|
+
* @param {{ installedDir: string, kitSourceDir: string, manifest: object|null }} params
|
|
238
|
+
* @returns {object} drift report (see module doc comment for shape)
|
|
239
|
+
*/
|
|
240
|
+
function compareSkillDrift({ installedDir, kitSourceDir, manifest }) {
|
|
241
|
+
const manifestFiles = manifest && manifest.files && typeof manifest.files === 'object' ? manifest.files : null;
|
|
242
|
+
|
|
243
|
+
const relPaths = new Set();
|
|
244
|
+
for (const file of walkFilesSorted(kitSourceDir)) relPaths.add(relPathPosix(kitSourceDir, file));
|
|
245
|
+
for (const file of walkFilesSorted(installedDir)) relPaths.add(relPathPosix(installedDir, file));
|
|
246
|
+
if (manifestFiles) {
|
|
247
|
+
for (const rel of Object.keys(manifestFiles)) {
|
|
248
|
+
// Defense-in-depth: our own writer (`buildManifest`) only ever produces POSIX-relative
|
|
249
|
+
// keys derived from `walkFilesSorted` + `relPathPosix`, so a key that is absolute, contains
|
|
250
|
+
// a backslash, or has any '..' path segment can only come from a malformed or hostile
|
|
251
|
+
// manifest — skip it entirely (never join it onto installedDir/kitSourceDir, never report
|
|
252
|
+
// it) rather than trusting untrusted manifest content into a path.join.
|
|
253
|
+
if (path.isAbsolute(rel) || rel.includes('\\') || rel.split('/').includes('..')) continue;
|
|
254
|
+
relPaths.add(rel);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const summary = { total: 0, inSync: 0, kitUpdated: 0, userModified: 0, unbaselined: 0, missingInstall: 0, kitRemoved: 0 };
|
|
259
|
+
const files = [];
|
|
260
|
+
|
|
261
|
+
for (const rel of Array.from(relPaths).sort()) {
|
|
262
|
+
const installedHash = hashRelIfPresent(installedDir, rel);
|
|
263
|
+
const kitHash = hashRelIfPresent(kitSourceDir, rel);
|
|
264
|
+
const manifestHash = manifestFiles && Object.prototype.hasOwnProperty.call(manifestFiles, rel) ? manifestFiles[rel] : null;
|
|
265
|
+
|
|
266
|
+
// A path recorded only in a stale manifest, with no counterpart in the kit source NOR the
|
|
267
|
+
// installed dir any more, is nothing currently installable or driftable — skip it rather
|
|
268
|
+
// than inventing a seventh state for it.
|
|
269
|
+
if (installedHash === null && kitHash === null) continue;
|
|
270
|
+
|
|
271
|
+
// An installed-only path with no kit-source counterpart AND no manifest entry is outside the
|
|
272
|
+
// kit's jurisdiction entirely — e.g. another tool's skill directory living alongside
|
|
273
|
+
// kit-installed skills under the same shared `~/.claude/skills` dest. `copyDirMerge`'s doc
|
|
274
|
+
// contract (`src/cli/init.ts`) is explicit that the dest may hold unrelated content it must
|
|
275
|
+
// never touch; the drift classifier honors the same boundary by staying silent about paths it
|
|
276
|
+
// never owned, rather than reporting them "unbaselined". `unbaselined` stays reserved for
|
|
277
|
+
// kit-owned paths (kitHash !== null) that simply predate the manifest baseline; `kit_removed`
|
|
278
|
+
// is unaffected — a manifest entry there proves prior kit ownership, so it never reaches this
|
|
279
|
+
// skip (manifestHash === null is required here).
|
|
280
|
+
if (kitHash === null && manifestHash === null) continue;
|
|
281
|
+
|
|
282
|
+
let state;
|
|
283
|
+
if (kitHash !== null && installedHash === null) {
|
|
284
|
+
state = 'missing_install';
|
|
285
|
+
} else if (installedHash !== null && kitHash !== null && installedHash === kitHash) {
|
|
286
|
+
state = 'in_sync';
|
|
287
|
+
} else if (manifestHash === null) {
|
|
288
|
+
state = 'unbaselined';
|
|
289
|
+
} else if (installedHash !== manifestHash) {
|
|
290
|
+
// user_modified always wins over kit_updated/kit_removed when installed bytes differ from
|
|
291
|
+
// the recorded baseline — see module doc comment. This precedence also covers the
|
|
292
|
+
// kitHash === null sub-case (file removed from kit source AND locally modified): still
|
|
293
|
+
// reported as user_modified, never kit_removed.
|
|
294
|
+
state = 'user_modified';
|
|
295
|
+
} else if (kitHash === null) {
|
|
296
|
+
// Installed file matches the last recorded baseline exactly, but no longer exists anywhere
|
|
297
|
+
// in the current kit source (removed upstream). Distinct from kit_updated: the additive
|
|
298
|
+
// copyDirMerge-based refresh cannot delete, so it cannot "fix" this state.
|
|
299
|
+
state = 'kit_removed';
|
|
300
|
+
} else {
|
|
301
|
+
state = 'kit_updated';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
files.push({ path: rel, state, installedHash, kitHash, manifestHash });
|
|
305
|
+
summary.total += 1;
|
|
306
|
+
if (state === 'in_sync') summary.inSync += 1;
|
|
307
|
+
else if (state === 'kit_updated') summary.kitUpdated += 1;
|
|
308
|
+
else if (state === 'user_modified') summary.userModified += 1;
|
|
309
|
+
else if (state === 'unbaselined') summary.unbaselined += 1;
|
|
310
|
+
else if (state === 'missing_install') summary.missingInstall += 1;
|
|
311
|
+
else if (state === 'kit_removed') summary.kitRemoved += 1;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
checkedAt: new Date().toISOString(),
|
|
316
|
+
installedDir,
|
|
317
|
+
kitSourceDir,
|
|
318
|
+
manifestFound: manifest !== null,
|
|
319
|
+
files,
|
|
320
|
+
summary,
|
|
321
|
+
hasDrift: files.some((entry) => entry.state !== 'in_sync'),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolves the global claude-code destination directory (e.g. `~/.claude`), honoring
|
|
327
|
+
* `FLOW_AGENTS_USER_CLAUDE_SETTINGS` for test isolation — the SAME resolution
|
|
328
|
+
* `src/cli/init.ts`'s `globalDest("claude-code")` already implements, re-implemented here in
|
|
329
|
+
* pure CJS so `workflow-steering.js` can call it directly without a `createRequire`-of-a-
|
|
330
|
+
* differently-shaped-compiled-path detour. `src/cli/skill-drift-check.ts` instead calls the
|
|
331
|
+
* real exported `globalDest` from `init.ts` directly (already TS-to-TS, no duplication needed
|
|
332
|
+
* there).
|
|
333
|
+
*
|
|
334
|
+
* @param {NodeJS.ProcessEnv} env
|
|
335
|
+
* @returns {string}
|
|
336
|
+
*/
|
|
337
|
+
function resolveClaudeGlobalSkillsDir(env = process.env) {
|
|
338
|
+
const override = env['FLOW_AGENTS_USER_CLAUDE_SETTINGS'];
|
|
339
|
+
if (override) return path.dirname(override);
|
|
340
|
+
return path.join(os.homedir(), '.claude');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
hashFile,
|
|
345
|
+
buildManifest,
|
|
346
|
+
loadManifest,
|
|
347
|
+
writeManifestAtomic,
|
|
348
|
+
compareSkillDrift,
|
|
349
|
+
resolveClaudeGlobalSkillsDir,
|
|
350
|
+
};
|