@polymorphism-tech/morph-spec 9.0.0 → 9.1.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/CLAUDE.md +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +4 -62
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +11 -67
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +34 -12
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +14 -96
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +28 -275
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared read-only state accessor for Claude Code hooks.
|
|
3
|
-
*
|
|
4
|
-
* Wraps state.json reads with fail-open semantics:
|
|
5
|
-
* - Returns null instead of throwing on missing/corrupt state
|
|
6
|
-
* - Never writes to state.json (read-only)
|
|
7
|
-
* - Uses synchronous reads for <100ms performance
|
|
8
|
-
*/
|
|
1
|
+
// Fail-open: missing or corrupt state reads as null, never throws.
|
|
9
2
|
|
|
10
3
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
11
4
|
import { join } from 'path';
|
|
@@ -13,80 +6,15 @@ import { join } from 'path';
|
|
|
13
6
|
const STATE_FILE = '.morph/state.json';
|
|
14
7
|
const FEATURES_DIR = '.morph/features';
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* `loadState()` does a `readdirSync` of `.morph/features` plus a
|
|
20
|
-
* `readFileSync` + `JSON.parse` of EVERY `feature.json` under it, on every
|
|
21
|
-
* call. A single hook invocation calls it 1-3x (directly, or via
|
|
22
|
-
* `getActiveFeature`/`derivePhaseForFeature`/`isGateApproved`/etc.) — re-doing
|
|
23
|
-
* that full hydration each time is pure waste when nothing on disk changed
|
|
24
|
-
* between those calls.
|
|
25
|
-
*
|
|
26
|
-
* SAFE because every hook process is short-lived and single-invocation: one
|
|
27
|
-
* `node state-reader.js`-importing process handles exactly one tool call (or
|
|
28
|
-
* one CLI command) and exits. There is no risk of the cache going stale
|
|
29
|
-
* ACROSS invocations, because each invocation starts with an empty Map.
|
|
30
|
-
* Within a SINGLE invocation, staleness would only matter if that same
|
|
31
|
-
* process both reads and then writes a feature's `feature.json` and
|
|
32
|
-
* expects a LATER read to see the write — verified (grep across every
|
|
33
|
-
* consumer, 2026-08-11) that the one hook that writes feature.json
|
|
34
|
-
* (`state-sync.js`) never re-reads through `loadState()` after writing in the
|
|
35
|
-
* same invocation; its `AskUserQuestion` handler reads via
|
|
36
|
-
* `readFeatureFile()` (a different, uncached module) as the PRIMARY path and
|
|
37
|
-
* only falls back to `loadState()` for a feature that has NEVER had a
|
|
38
|
-
* feature.json written — which, by construction, can't have just been
|
|
39
|
-
* written to in this same invocation either.
|
|
40
|
-
*
|
|
41
|
-
* A directory-mtime-based invalidation was considered instead (so a
|
|
42
|
-
* LONG-LIVED consumer could reuse this module safely too) but rejected on two
|
|
43
|
-
* grounds: (1) verified — grep for every importer of this file — that none is
|
|
44
|
-
* long-lived (they're all hooks, one process per tool call, or CLI commands,
|
|
45
|
-
* one process per invocation; the dashboard is a separate Vite/React app and
|
|
46
|
-
* the statusline is Python, neither imports this module); and (2) even if one
|
|
47
|
-
* existed, `.morph/features`'s own mtime only changes when feature
|
|
48
|
-
* DIRECTORIES are added/removed, not when an EXISTING feature.json's content
|
|
49
|
-
* is overwritten in place — so mtime-of-the-dir can't actually detect the
|
|
50
|
-
* mutation that matters here, and getting that right would mean stat'ing
|
|
51
|
-
* every feature.json individually, which re-adds the very cost this cache
|
|
52
|
-
* exists to remove. If a long-lived consumer is ever added, it should
|
|
53
|
-
* re-derive state per request rather than lean on this cache.
|
|
54
|
-
*
|
|
55
|
-
* CAVEAT for any future consumer: the object returned by `loadState()` is the
|
|
56
|
-
* SAME object on every cache hit, not a fresh copy — a caller that mutates it
|
|
57
|
-
* (as `state-sync.js`'s `handleWriteEdit` does, in-place, before persisting)
|
|
58
|
-
* is mutating the cached entry too. That's harmless today because nothing
|
|
59
|
-
* currently reads the cache again after such a mutation within one process,
|
|
60
|
-
* but treat the return value as read-only unless you've re-verified that.
|
|
61
|
-
*
|
|
62
|
-
* `clearStateCache()` is exported for tests that write/rewrite feature.json
|
|
63
|
-
* and then call a `loadState()`-backed function again WITHIN THE SAME
|
|
64
|
-
* process and need to see the fresh content — call it at the point of
|
|
65
|
-
* mutation.
|
|
66
|
-
*/
|
|
9
|
+
// Memoized per process: each hook/CLI process handles one call. The returned
|
|
10
|
+
// object is shared, so treat it as read-only; writers call `clearStateCache()`.
|
|
67
11
|
const stateCache = new Map();
|
|
68
12
|
|
|
69
|
-
/** Drop all cached state — see `stateCache` above for when this is needed. */
|
|
70
13
|
export function clearStateCache() {
|
|
71
14
|
stateCache.clear();
|
|
72
15
|
}
|
|
73
16
|
|
|
74
|
-
/**
|
|
75
|
-
* Load state from the project root, hydrating each feature from its
|
|
76
|
-
* authoritative per-feature `feature.json`.
|
|
77
|
-
*
|
|
78
|
-
* The root `.morph/state.json` is a thin, gitignored index (status + updatedAt
|
|
79
|
-
* per feature); the heavy feature data lives in `.morph/features/{name}/feature.json`
|
|
80
|
-
* (committed). This reader merges them: it reads the thin index for project /
|
|
81
|
-
* metadata and overlays each feature.json so callers see the full feature object.
|
|
82
|
-
* A single corrupt feature.json is skipped, not fatal.
|
|
83
|
-
*
|
|
84
|
-
* Fail-open: returns null on any unexpected error or when there is no state at
|
|
85
|
-
* all (no index and no feature files). Memoized per `projectPath` for the
|
|
86
|
-
* lifetime of this process — see `stateCache` above.
|
|
87
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
88
|
-
* @returns {Object|null} State object or null
|
|
89
|
-
*/
|
|
17
|
+
/** The thin `state.json` index overlaid with each authoritative `feature.json`; null when neither exists. */
|
|
90
18
|
export function loadState(projectPath) {
|
|
91
19
|
const base = projectPath || process.cwd();
|
|
92
20
|
if (stateCache.has(base)) return stateCache.get(base);
|
|
@@ -119,7 +47,7 @@ export function loadState(projectPath) {
|
|
|
119
47
|
state.features = state.features || {};
|
|
120
48
|
state.features[name] = fj; // feature.json is authoritative
|
|
121
49
|
} catch {
|
|
122
|
-
//
|
|
50
|
+
// one corrupt feature.json must not blind every hook
|
|
123
51
|
}
|
|
124
52
|
}
|
|
125
53
|
}
|
|
@@ -134,17 +62,8 @@ export function loadState(projectPath) {
|
|
|
134
62
|
return state;
|
|
135
63
|
}
|
|
136
64
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* exists, or `.morph/features/` (committed, gitignore-exempt) already has at
|
|
140
|
-
* least one feature with a `feature.json`. The latter case covers a freshly
|
|
141
|
-
* checked-out worktree where state.json (gitignored) hasn't been materialized
|
|
142
|
-
* yet, but `loadState()` can already self-heal from feature.json — so hooks
|
|
143
|
-
* gating on `stateExists()` should be "on" there too, not dark.
|
|
144
|
-
* Fail-open: any unexpected error (e.g. permission denied) returns false.
|
|
145
|
-
* @param {string} [projectPath]
|
|
146
|
-
* @returns {boolean}
|
|
147
|
-
*/
|
|
65
|
+
// A fresh worktree has `feature.json` but no gitignored `state.json` yet; hooks
|
|
66
|
+
// must stay on there.
|
|
148
67
|
export function stateExists(projectPath) {
|
|
149
68
|
try {
|
|
150
69
|
const base = projectPath || process.cwd();
|
|
@@ -163,11 +82,6 @@ export function stateExists(projectPath) {
|
|
|
163
82
|
}
|
|
164
83
|
}
|
|
165
84
|
|
|
166
|
-
/**
|
|
167
|
-
* Get the active feature (most recently updated in_progress feature).
|
|
168
|
-
* @param {string} [projectPath]
|
|
169
|
-
* @returns {{ name: string, feature: Object }|null}
|
|
170
|
-
*/
|
|
171
85
|
export function getActiveFeature(projectPath) {
|
|
172
86
|
const state = loadState(projectPath);
|
|
173
87
|
if (!state?.features) return null;
|
|
@@ -188,12 +102,6 @@ export function getActiveFeature(projectPath) {
|
|
|
188
102
|
return active;
|
|
189
103
|
}
|
|
190
104
|
|
|
191
|
-
/**
|
|
192
|
-
* Get the most recently updated feature (any status).
|
|
193
|
-
* Useful as a fallback when getActiveFeature returns null (all features done).
|
|
194
|
-
* @param {string} [projectPath]
|
|
195
|
-
* @returns {{ name: string, feature: Object }|null}
|
|
196
|
-
*/
|
|
197
105
|
export function getMostRecentFeature(projectPath) {
|
|
198
106
|
const state = loadState(projectPath);
|
|
199
107
|
if (!state?.features) return null;
|
|
@@ -212,39 +120,15 @@ export function getMostRecentFeature(projectPath) {
|
|
|
212
120
|
return result;
|
|
213
121
|
}
|
|
214
122
|
|
|
215
|
-
/**
|
|
216
|
-
* Get a specific feature by name.
|
|
217
|
-
* @param {string} featureName
|
|
218
|
-
* @param {string} [projectPath]
|
|
219
|
-
* @returns {Object|null}
|
|
220
|
-
*/
|
|
221
123
|
export function getFeature(featureName, projectPath) {
|
|
222
124
|
const state = loadState(projectPath);
|
|
223
125
|
return state?.features?.[featureName] || null;
|
|
224
126
|
}
|
|
225
127
|
|
|
226
|
-
/**
|
|
227
|
-
* Derive phase from filesystem (phase deleted from state).
|
|
228
|
-
*
|
|
229
|
-
* Walks the phase map in descending order and returns the first phase whose
|
|
230
|
-
* CANONICAL ARTIFACT exists. An empty folder is not enough — a pre-created
|
|
231
|
-
* sibling folder never advances the phase before its content is written.
|
|
232
|
-
*
|
|
233
|
-
* Canonical artifacts (one per phase) match MORPH.md §3:
|
|
234
|
-
* - 4-review → verifier.md
|
|
235
|
-
* - 3-implement → any slice report (`reports/*.md`)
|
|
236
|
-
* - 1-design → design-system.md
|
|
237
|
-
* - 2-plan → plan.md
|
|
238
|
-
*
|
|
239
|
-
* If no canonical file is found, returns `'plan'` (the starting phase).
|
|
240
|
-
*/
|
|
241
128
|
export const PHASE_INDEX = { plan: 0, uiux: 1, implement: 2, review: 3 };
|
|
242
129
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
* holds the file list from somewhere OTHER than the local filesystem (a git
|
|
246
|
-
* tree, say) can answer the same question without re-encoding the map.
|
|
247
|
-
*/
|
|
130
|
+
// Descending. An empty folder is not an artifact: a pre-created folder must
|
|
131
|
+
// not advance the phase.
|
|
248
132
|
export const PHASE_ARTIFACTS = [
|
|
249
133
|
['4-review', 'review', 'verifier.md'],
|
|
250
134
|
['3-implement', 'implement', 'reports/'],
|
|
@@ -256,18 +140,6 @@ const DESIGN_DIR = '1-design';
|
|
|
256
140
|
|
|
257
141
|
const gatesOf = (feature) => feature?.approvals || feature?.approvalGates;
|
|
258
142
|
|
|
259
|
-
/**
|
|
260
|
-
* Gate-implied phase from an ALREADY-LOADED feature object — no disk access.
|
|
261
|
-
* An approved gate authorizes the NEXT phase:
|
|
262
|
-
* review approved → review (terminal)
|
|
263
|
-
* plan approved → implement, or uiux while the design branch was taken
|
|
264
|
-
* (`1-design/` exists) and its gate is still open
|
|
265
|
-
* otherwise → plan
|
|
266
|
-
*
|
|
267
|
-
* @param {Object|null} feature
|
|
268
|
-
* @param {boolean} [designStarted] - whether `1-design/` exists for the feature
|
|
269
|
-
* @returns {string} phase name
|
|
270
|
-
*/
|
|
271
143
|
export function gateImpliedPhaseFrom(feature, designStarted = false) {
|
|
272
144
|
const gates = gatesOf(feature);
|
|
273
145
|
const ok = (g) => gates?.[g]?.approved === true;
|
|
@@ -276,10 +148,7 @@ export function gateImpliedPhaseFrom(feature, designStarted = false) {
|
|
|
276
148
|
return 'plan';
|
|
277
149
|
}
|
|
278
150
|
|
|
279
|
-
/**
|
|
280
|
-
* Artifact predicate over a feature dir on disk. A path ending in `/` asks for
|
|
281
|
-
* any `.md` file directly under it: an empty folder is not an artifact.
|
|
282
|
-
*/
|
|
151
|
+
/** A path ending in `/` asks for any `.md` file directly under it. */
|
|
283
152
|
export function diskArtifactProbe(featureDir) {
|
|
284
153
|
return (rel) => {
|
|
285
154
|
const abs = join(featureDir, ...rel.split('/').filter(Boolean));
|
|
@@ -292,7 +161,6 @@ export function diskArtifactProbe(featureDir) {
|
|
|
292
161
|
};
|
|
293
162
|
}
|
|
294
163
|
|
|
295
|
-
/** The same predicate over a set of feature-relative POSIX paths (a git tree). */
|
|
296
164
|
export function fileSetArtifactProbe(files) {
|
|
297
165
|
return (rel) => {
|
|
298
166
|
if (!rel.endsWith('/')) return files.has(rel);
|
|
@@ -303,30 +171,20 @@ export function fileSetArtifactProbe(files) {
|
|
|
303
171
|
};
|
|
304
172
|
}
|
|
305
173
|
|
|
306
|
-
/**
|
|
307
|
-
* Phase from an already-loaded feature plus an artifact predicate — no disk
|
|
308
|
-
* access of its own, so the artifact universe can be a git tree.
|
|
309
|
-
*
|
|
310
|
-
* @param {Object|null} feature
|
|
311
|
-
* @param {(relPath: string) => boolean} hasArtifact - {@link diskArtifactProbe}
|
|
312
|
-
* or {@link fileSetArtifactProbe}: POSIX paths relative to the feature dir
|
|
313
|
-
* @returns {string} phase name
|
|
314
|
-
*/
|
|
174
|
+
/** @param {(relPath: string) => boolean} hasArtifact - {@link diskArtifactProbe} or {@link fileSetArtifactProbe} */
|
|
315
175
|
export function derivePhaseFrom(feature, hasArtifact) {
|
|
316
176
|
let artifactPhase = 'plan';
|
|
317
177
|
for (const [folder, phase, canonical] of PHASE_ARTIFACTS) {
|
|
318
178
|
if (hasArtifact(`${folder}/${canonical}`)) { artifactPhase = phase; break; }
|
|
319
179
|
}
|
|
320
180
|
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
// written. Take whichever of (artifact, gate-implied) is later.
|
|
181
|
+
// An approved gate advances the phase before the next artifact exists, or
|
|
182
|
+
// that artifact could never be written.
|
|
324
183
|
const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/design-system.md`);
|
|
325
184
|
const implied = gateImpliedPhaseFrom(feature, designStarted);
|
|
326
185
|
return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
|
|
327
186
|
}
|
|
328
187
|
|
|
329
|
-
/** The phase implied purely by approved gates — see {@link gateImpliedPhaseFrom}. */
|
|
330
188
|
export function gateImpliedPhase(featureName, projectPath) {
|
|
331
189
|
const designDir = join(projectPath || process.cwd(), '.morph/features', featureName, DESIGN_DIR);
|
|
332
190
|
return gateImpliedPhaseFrom(getFeature(featureName, projectPath), existsSync(designDir));
|
|
@@ -337,41 +195,19 @@ export function derivePhaseForFeature(featureName, projectPath) {
|
|
|
337
195
|
return derivePhaseFrom(getFeature(featureName, projectPath), diskArtifactProbe(basePath));
|
|
338
196
|
}
|
|
339
197
|
|
|
340
|
-
/**
|
|
341
|
-
* Get the current phase of a feature.
|
|
342
|
-
* Falls back to filesystem derivation when phase is not in state.
|
|
343
|
-
* @param {string} featureName
|
|
344
|
-
* @param {string} [projectPath]
|
|
345
|
-
* @returns {string|null}
|
|
346
|
-
*/
|
|
347
198
|
export function getFeaturePhase(featureName, projectPath) {
|
|
348
199
|
const feature = getFeature(featureName, projectPath);
|
|
349
200
|
if (!feature) return null;
|
|
350
201
|
return feature.phase || derivePhaseForFeature(featureName, projectPath);
|
|
351
202
|
}
|
|
352
203
|
|
|
353
|
-
/**
|
|
354
|
-
* Get approval status for a gate.
|
|
355
|
-
* @param {string} featureName
|
|
356
|
-
* @param {string} gate - 'plan'|'uiux'|'review'
|
|
357
|
-
* @param {string} [projectPath]
|
|
358
|
-
* @returns {boolean}
|
|
359
|
-
*/
|
|
360
204
|
export function isGateApproved(featureName, gate, projectPath) {
|
|
361
205
|
const feature = getFeature(featureName, projectPath);
|
|
362
|
-
// Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
|
|
363
206
|
const gates = feature?.approvals || feature?.approvalGates;
|
|
364
207
|
return gates?.[gate]?.approved === true;
|
|
365
208
|
}
|
|
366
209
|
|
|
367
|
-
/**
|
|
368
|
-
* Get all pending approval gates for a feature.
|
|
369
|
-
* @param {string} featureName
|
|
370
|
-
* @param {string} [projectPath]
|
|
371
|
-
* @returns {string[]} List of gate names that are not yet approved
|
|
372
|
-
*/
|
|
373
210
|
export function pendingGatesFrom(feature) {
|
|
374
|
-
// Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
|
|
375
211
|
const gates = gatesOf(feature);
|
|
376
212
|
if (!gates) return [];
|
|
377
213
|
|
|
@@ -384,12 +220,6 @@ export function getPendingGates(featureName, projectPath) {
|
|
|
384
220
|
return pendingGatesFrom(getFeature(featureName, projectPath));
|
|
385
221
|
}
|
|
386
222
|
|
|
387
|
-
/**
|
|
388
|
-
* Get feature outputs status.
|
|
389
|
-
* @param {string} featureName
|
|
390
|
-
* @param {string} [projectPath]
|
|
391
|
-
* @returns {Object|null} outputs map or null
|
|
392
|
-
*/
|
|
393
223
|
export function getOutputs(featureName, projectPath) {
|
|
394
224
|
const feature = getFeature(featureName, projectPath);
|
|
395
225
|
return feature?.outputs || null;
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Hooks run inside the user's project and cannot import the CLI's
|
|
5
|
-
* src/core/state-manager.js. This module is the hook-side mirror of the core
|
|
6
|
-
* writers: it persists a feature to its authoritative `feature.json` (committed)
|
|
7
|
-
* and refreshes the thin, gitignored root index (`.morph/state.json`).
|
|
8
|
-
*
|
|
9
|
-
* Fail-open by contract: every function swallows errors and returns a boolean —
|
|
10
|
-
* a hook must never break the agent over a state write.
|
|
11
|
-
*/
|
|
1
|
+
// Hook-side mirror of the CLI state writers (hooks cannot import `src/`).
|
|
2
|
+
// Fail-open: every function returns a boolean, never throws.
|
|
12
3
|
|
|
13
4
|
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
14
5
|
import { join, dirname } from 'path';
|
|
@@ -17,12 +8,10 @@ import { clearStateCache } from './state-reader.js';
|
|
|
17
8
|
const STATE_FILE = '.morph/state.json';
|
|
18
9
|
const STATE_VERSION = '5.0.0';
|
|
19
10
|
|
|
20
|
-
/** Absolute path to a feature's authoritative feature.json. */
|
|
21
11
|
export function featureJsonPath(featureName, projectPath) {
|
|
22
12
|
return join(projectPath || process.cwd(), '.morph', 'features', featureName, 'feature.json');
|
|
23
13
|
}
|
|
24
14
|
|
|
25
|
-
/** Read a feature's feature.json, or null if missing/corrupt. */
|
|
26
15
|
export function readFeatureFile(featureName, projectPath) {
|
|
27
16
|
const p = featureJsonPath(featureName, projectPath);
|
|
28
17
|
if (!existsSync(p)) return null;
|
|
@@ -33,7 +22,7 @@ export function readFeatureFile(featureName, projectPath) {
|
|
|
33
22
|
}
|
|
34
23
|
}
|
|
35
24
|
|
|
36
|
-
|
|
25
|
+
// EPERM retry: Windows rename fails while another process holds the target.
|
|
37
26
|
function atomicWriteJson(targetPath, data) {
|
|
38
27
|
const dir = dirname(targetPath);
|
|
39
28
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
@@ -54,11 +43,6 @@ function atomicWriteJson(targetPath, data) {
|
|
|
54
43
|
}
|
|
55
44
|
}
|
|
56
45
|
|
|
57
|
-
/**
|
|
58
|
-
* Persist a feature to its authoritative feature.json and refresh the thin root
|
|
59
|
-
* index entry (status + updatedAt) + aggregate metadata. Returns true on
|
|
60
|
-
* success, false fail-open.
|
|
61
|
-
*/
|
|
62
46
|
export function persistFeature(featureName, feature, projectPath) {
|
|
63
47
|
try {
|
|
64
48
|
atomicWriteJson(featureJsonPath(featureName, projectPath), feature);
|
|
@@ -75,19 +59,8 @@ export function persistFeature(featureName, feature, projectPath) {
|
|
|
75
59
|
root.metadata.totalFeatures = Object.keys(root.features).length;
|
|
76
60
|
atomicWriteJson(statePath, root);
|
|
77
61
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// process (README history), but core/chain.js now runs multiple hooks
|
|
81
|
-
// — e.g. state-sync then gate-guard — IN ONE process. Without this,
|
|
82
|
-
// a loadState() call earlier in that same process (state-sync's
|
|
83
|
-
// handleWriteEdit reads via loadState before this write) would leave a
|
|
84
|
-
// stale snapshot cached, and gate-guard's loadState() right after this
|
|
85
|
-
// write would silently read the PRE-write state instead of what was
|
|
86
|
-
// just persisted — inverting the "gate-guard runs after state-sync so it
|
|
87
|
-
// sees the gate state-sync just recorded" ordering guarantee the
|
|
88
|
-
// registry comment documents. Invalidating here (the one place
|
|
89
|
-
// feature.json/state.json are actually written) covers every current
|
|
90
|
-
// and future caller, not just this one chain.
|
|
62
|
+
// chain.js runs several hooks in one process: the next link must not read
|
|
63
|
+
// the memoized pre-write snapshot.
|
|
91
64
|
clearStateCache();
|
|
92
65
|
return true;
|
|
93
66
|
} catch {
|
|
@@ -95,16 +68,7 @@ export function persistFeature(featureName, feature, projectPath) {
|
|
|
95
68
|
}
|
|
96
69
|
}
|
|
97
70
|
|
|
98
|
-
/**
|
|
99
|
-
* Canonical gate-approval writer (hook-side mirror of CLI setApprovalGate).
|
|
100
|
-
* Stamps approvalGates[gate] = { approved, timestamp, approvedBy } — the single
|
|
101
|
-
* canonical shape — onto the feature and persists it. Returns true on success.
|
|
102
|
-
*
|
|
103
|
-
* @param {string} featureName
|
|
104
|
-
* @param {string} gate - 'plan'|'uiux'|'review'
|
|
105
|
-
* @param {{ approvedBy?: string, timestamp?: string }} [meta]
|
|
106
|
-
* @param {string} [projectPath]
|
|
107
|
-
*/
|
|
71
|
+
/** @param {{ approvedBy?: string, timestamp?: string, selfAssessed?: boolean }} [meta] */
|
|
108
72
|
export function setGateApproved(featureName, gate, meta = {}, projectPath) {
|
|
109
73
|
try {
|
|
110
74
|
const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature(featureName);
|
|
@@ -123,16 +87,8 @@ export function setGateApproved(featureName, gate, meta = {}, projectPath) {
|
|
|
123
87
|
}
|
|
124
88
|
}
|
|
125
89
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* Stamps identity — name (the slug), workType, createdAt — so the implicit
|
|
129
|
-
* birth path (state-sync registering a feature on the first Write) never
|
|
130
|
-
* produces an anonymous {status,updatedAt} shell. `description` stays absent
|
|
131
|
-
* (a hook can't invent it); `list`/`doctor` flag its absence. Mirrors the CLI
|
|
132
|
-
* createFeature's stamping.
|
|
133
|
-
*
|
|
134
|
-
* @param {string} [featureName] - the feature slug, stamped as `name`
|
|
135
|
-
*/
|
|
90
|
+
// Stamps identity so a feature born implicitly by a hook is never an anonymous
|
|
91
|
+
// shell; `description` stays absent because a hook cannot invent it.
|
|
136
92
|
export function createDefaultFeature(featureName) {
|
|
137
93
|
const now = new Date().toISOString();
|
|
138
94
|
return {
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared stdin reader for Claude Code hooks.
|
|
3
|
-
*
|
|
4
|
-
* Reads JSON payload from stdin (provided by Claude Code to hooks).
|
|
5
|
-
* Returns parsed object or null on failure.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Read and parse JSON from stdin
|
|
10
|
-
* @returns {Promise<Object|null>} Parsed JSON or null
|
|
11
|
-
*/
|
|
1
|
+
/** @returns {Promise<Object|null>} the hook payload, or null when stdin is not JSON */
|
|
12
2
|
export function readStdin() {
|
|
13
3
|
return new Promise((resolve) => {
|
|
14
4
|
let input = '';
|
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Writes one JSON line per event to .morph/logs/events.jsonl. This is the sink
|
|
5
|
-
* for the `telemetry-log` hook, which fires on every tool call (PostToolUse,
|
|
6
|
-
* all tools) and on every user prompt (UserPromptSubmit) — the only vantage
|
|
7
|
-
* points an NLH exposes. The whole execution path (skills, sub-agent dispatch,
|
|
8
|
-
* knowledge-base reads, prompts) is reconstructable by reading this file.
|
|
9
|
-
*
|
|
10
|
-
* Design constraints (same discipline as activity-logger, plus one):
|
|
11
|
-
* - Synchronous only (hooks run in process, no async overhead)
|
|
12
|
-
* - Fail-silent (never throws — hooks must not be blocked by the logger)
|
|
13
|
-
* - APPEND-ONLY: one `appendFileSync` line per event, O(1) per call. The
|
|
14
|
-
* read-modify-write of a whole JSON doc (activity-logger) is O(n²) across a
|
|
15
|
-
* session and blows the ≤5ms budget once tool calls number in the hundreds.
|
|
16
|
-
* - Windows-safe paths via path.join
|
|
17
|
-
*/
|
|
1
|
+
// Append-only on purpose: rewriting a whole JSON doc per event is O(n²) across
|
|
2
|
+
// a session, and this runs on every tool call. Fail-silent: never blocks a hook.
|
|
18
3
|
|
|
19
4
|
import {
|
|
20
5
|
appendFileSync, readFileSync, mkdirSync, existsSync, statSync, renameSync,
|
|
@@ -26,13 +11,7 @@ import { redactSecrets } from './redact-secrets.js';
|
|
|
26
11
|
const LOGS_DIR = '.morph/logs';
|
|
27
12
|
export const EVENTS_FILE = join(LOGS_DIR, 'events.jsonl');
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
// this size, alongside the pre-existing activity.jsonl rotation — same
|
|
31
|
-
// call site, same "between sessions" checkpoint. events.jsonl has no natural
|
|
32
|
-
// per-turn cap (prompts/commands are truncated at the write site, but the
|
|
33
|
-
// LINE COUNT itself grows unbounded across a long-lived project), so without
|
|
34
|
-
// this it grows forever the way activity.jsonl used to before R1.2.
|
|
35
|
-
export const ROTATE_THRESHOLD_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
14
|
+
export const ROTATE_THRESHOLD_BYTES = 5 * 1024 * 1024;
|
|
36
15
|
|
|
37
16
|
// Bumped when a field changes meaning. Readers key legacy handling off its
|
|
38
17
|
// absence (v1 events carry no schemaVersion and no eventId).
|
|
@@ -49,15 +28,7 @@ function getEventsPath(projectPath) {
|
|
|
49
28
|
return join(projectPath || process.cwd(), EVENTS_FILE);
|
|
50
29
|
}
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
* Append a single telemetry event as one JSON line.
|
|
54
|
-
* Stamps `ts` (unless given), `schemaVersion` and a unique `eventId` — the
|
|
55
|
-
* dedupe key when the same line is read back from a rotated archive or a
|
|
56
|
-
* finish backup — and redacts secrets in the free-text fields.
|
|
57
|
-
*
|
|
58
|
-
* @param {Object} event - Event payload (tool, skill/persona/target/prompt, feature, phase…)
|
|
59
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
60
|
-
*/
|
|
31
|
+
// `eventId` dedupes the same line read back from a rotated archive or a finish backup.
|
|
61
32
|
export function appendTelemetryEvent(event, projectPath) {
|
|
62
33
|
try {
|
|
63
34
|
const eventsPath = getEventsPath(projectPath);
|
|
@@ -75,17 +46,9 @@ export function appendTelemetryEvent(event, projectPath) {
|
|
|
75
46
|
}
|
|
76
47
|
appendFileSync(eventsPath, JSON.stringify(stamped) + '\n', 'utf-8');
|
|
77
48
|
} catch {
|
|
78
|
-
// Fail-silent — never block hooks
|
|
79
49
|
}
|
|
80
50
|
}
|
|
81
51
|
|
|
82
|
-
/**
|
|
83
|
-
* Read the full event stream back into an array. A corrupt/truncated line
|
|
84
|
-
* (e.g. a killed process mid-write) is skipped, not fatal.
|
|
85
|
-
*
|
|
86
|
-
* @param {string} [projectPath]
|
|
87
|
-
* @returns {Object[]} events in write order (may be empty)
|
|
88
|
-
*/
|
|
89
52
|
export function readTelemetryEvents(projectPath) {
|
|
90
53
|
try {
|
|
91
54
|
const eventsPath = getEventsPath(projectPath);
|
|
@@ -98,7 +61,7 @@ export function readTelemetryEvents(projectPath) {
|
|
|
98
61
|
try {
|
|
99
62
|
events.push(JSON.parse(trimmed));
|
|
100
63
|
} catch {
|
|
101
|
-
//
|
|
64
|
+
// a process killed mid-write leaves a truncated line
|
|
102
65
|
}
|
|
103
66
|
}
|
|
104
67
|
return events;
|
|
@@ -107,19 +70,7 @@ export function readTelemetryEvents(projectPath) {
|
|
|
107
70
|
}
|
|
108
71
|
}
|
|
109
72
|
|
|
110
|
-
|
|
111
|
-
* Archive the current events.jsonl to `.morph/logs/sessions/`, mirroring
|
|
112
|
-
* activity-logger.js's `archiveSession()` — same directory, same rename-not-
|
|
113
|
-
* rewrite mechanics (O(1) even at multi-MB sizes). Named with an `events-`
|
|
114
|
-
* prefix (activity-logger's archives carry no prefix) so the two streams
|
|
115
|
-
* never collide inside the shared `sessions/` directory and stay
|
|
116
|
-
* distinguishable at a glance.
|
|
117
|
-
*
|
|
118
|
-
* Fail-silent — called only from `rotateEventsLogIfLarge` below, which has
|
|
119
|
-
* already confirmed the file exists.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} [projectPath]
|
|
122
|
-
*/
|
|
73
|
+
// `events-` prefix: activity-logger archives share `sessions/` without one.
|
|
123
74
|
function archiveEvents(projectPath) {
|
|
124
75
|
try {
|
|
125
76
|
const eventsPath = getEventsPath(projectPath);
|
|
@@ -132,21 +83,10 @@ function archiveEvents(projectPath) {
|
|
|
132
83
|
const archivePath = join(sessionsDir, `events-${stamp}.jsonl`);
|
|
133
84
|
renameSync(eventsPath, archivePath);
|
|
134
85
|
} catch {
|
|
135
|
-
// Fail-silent
|
|
136
86
|
}
|
|
137
87
|
}
|
|
138
88
|
|
|
139
|
-
/**
|
|
140
|
-
* Rotate events.jsonl when it crosses `thresholdBytes`. Called from
|
|
141
|
-
* worktree-context.js on SessionStart, right next to the existing
|
|
142
|
-
* `rotateActivityLogIfLarge()` call — the only reliable "between sessions"
|
|
143
|
-
* checkpoint the harness gets. `thresholdBytes` is injectable so a test can
|
|
144
|
-
* force rotation without writing multiple megabytes.
|
|
145
|
-
*
|
|
146
|
-
* @param {string} [projectPath] - Project root (defaults to cwd)
|
|
147
|
-
* @param {number} [thresholdBytes] - Rotate once the log is at least this large
|
|
148
|
-
* @returns {boolean} true if a rotation was performed
|
|
149
|
-
*/
|
|
89
|
+
/** @returns {boolean} true if a rotation was performed */
|
|
150
90
|
export function rotateEventsLogIfLarge(projectPath, thresholdBytes = ROTATE_THRESHOLD_BYTES) {
|
|
151
91
|
try {
|
|
152
92
|
const eventsPath = getEventsPath(projectPath);
|
|
@@ -1,28 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Stop hook (the main session's transcript) and SubagentStop (each
|
|
4
|
-
* sub-agent's own transcript, which the main one does not include).
|
|
5
|
-
*/
|
|
1
|
+
// Shared by Stop and SubagentStop: the main transcript does not include the
|
|
2
|
+
// sub-agents' usage.
|
|
6
3
|
|
|
7
4
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
8
5
|
|
|
9
|
-
//
|
|
10
|
-
// than risk an unbounded read inside a hook.
|
|
6
|
+
// Skip rather than risk an unbounded read inside a hook.
|
|
11
7
|
const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024;
|
|
12
8
|
|
|
13
9
|
export const USAGE_KEYS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreationTokens'];
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* One turn is written as several lines (one per content block), each repeating
|
|
19
|
-
* the same usage under the same `message.id`: summing lines double-counts, so
|
|
20
|
-
* the sum is deduplicated by message id.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} [transcriptPath]
|
|
23
|
-
* @returns {{inputTokens:number,outputTokens:number,cacheReadTokens:number,cacheCreationTokens:number}|null}
|
|
24
|
-
* null when there is nothing to report (no/oversized/unreadable file, no usage)
|
|
25
|
-
*/
|
|
11
|
+
// One turn spans several lines repeating the same usage under the same
|
|
12
|
+
// `message.id`: summing lines double-counts, so the sum dedups by id.
|
|
26
13
|
export function readTranscriptUsage(transcriptPath) {
|
|
27
14
|
if (!transcriptPath || typeof transcriptPath !== 'string') return null;
|
|
28
15
|
try {
|
|
@@ -61,17 +48,8 @@ export function readTranscriptUsage(transcriptPath) {
|
|
|
61
48
|
}
|
|
62
49
|
}
|
|
63
50
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
* A counter going DOWN means the transcript restarted (a new file under the
|
|
68
|
-
* same key), so the whole current total is new usage — subtracting would
|
|
69
|
-
* produce negative spend.
|
|
70
|
-
*
|
|
71
|
-
* @param {object|null|undefined} previous - last recorded totals for this key
|
|
72
|
-
* @param {object} current - totals just read
|
|
73
|
-
* @returns {{delta: object, changed: boolean}}
|
|
74
|
-
*/
|
|
51
|
+
// A counter going DOWN means the transcript restarted: the whole current
|
|
52
|
+
// total is new usage, and subtracting would produce negative spend.
|
|
75
53
|
export function usageDelta(previous, current) {
|
|
76
54
|
const reset = !previous || USAGE_KEYS.some((k) => (current[k] ?? 0) < (previous[k] ?? 0));
|
|
77
55
|
const delta = {};
|