@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,21 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* heavy-release.js — PostToolUse / PostToolUseFailure link: gives back the slot `heavy-gate` took
|
|
4
|
-
* for this Bash call, matched by `tool_use_id`.
|
|
5
|
-
*
|
|
6
|
-
* Silent and fail-open. A call that fires neither event (refused by a rule, interrupted) leaves its
|
|
7
|
-
* slot to the TTL. Releasing is not deciding, so this link is safe in the async failure binding.
|
|
8
|
-
*/
|
|
2
|
+
// A call that fires neither Post event leaves its slot to the TTL. Releasing is not deciding, so this link is safe async.
|
|
9
3
|
|
|
10
4
|
import { existsSync } from 'node:fs';
|
|
11
5
|
import { stateExists } from '../../shared/state-reader.js';
|
|
12
6
|
import { runStandalone } from '../../shared/hook-response.js';
|
|
13
7
|
|
|
14
|
-
/**
|
|
15
|
-
* @param {object} payload - PostToolUse or PostToolUseFailure stdin payload
|
|
16
|
-
* @param {{ env?: object }} [deps] - test seam
|
|
17
|
-
* @returns {Promise<null>}
|
|
18
|
-
*/
|
|
19
8
|
export async function run(payload, deps = {}) {
|
|
20
9
|
try {
|
|
21
10
|
if (payload?.tool_name !== 'Bash' || !payload.tool_use_id) return null;
|
|
@@ -27,7 +16,7 @@ export async function run(payload, deps = {}) {
|
|
|
27
16
|
if (!existsSync(dir)) return null;
|
|
28
17
|
const { releaseLeaseById } = await import('../../shared/heavy-slots.js');
|
|
29
18
|
releaseLeaseById(dir, payload.tool_use_id);
|
|
30
|
-
} catch {
|
|
19
|
+
} catch {}
|
|
31
20
|
return null;
|
|
32
21
|
}
|
|
33
22
|
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* PostToolUse Hook: Loop Detection
|
|
5
|
-
*
|
|
6
|
-
* Event: PostToolUse | Matcher: none (all tools)
|
|
7
|
-
*
|
|
8
|
-
* Detects when an agent is stuck in a repetitive loop by tracking
|
|
9
|
-
* the last 15 actions. If the last 8 actions have <= 2 unique
|
|
10
|
-
* signatures, injects a warning with escape instructions.
|
|
11
|
-
*
|
|
12
|
-
* Cooldown: 5 minutes between warnings to prevent meta-loop.
|
|
13
|
-
*
|
|
14
|
-
* Fail-open: exits 0 on any error.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
18
4
|
import { join } from 'path';
|
|
19
5
|
import { stateExists } from '../../shared/state-reader.js';
|
|
@@ -24,13 +10,8 @@ import { logEvent } from '../../shared/activity-logger.js';
|
|
|
24
10
|
const MAX_BUFFER = 15;
|
|
25
11
|
const LOOP_WINDOW = 8;
|
|
26
12
|
const MAX_UNIQUE_SIGS = 2;
|
|
27
|
-
const COOLDOWN_MS = 5 * 60 * 1000;
|
|
13
|
+
const COOLDOWN_MS = 5 * 60 * 1000;
|
|
28
14
|
|
|
29
|
-
/**
|
|
30
|
-
* @param {object} payload - parsed PostToolUse stdin payload (any tool)
|
|
31
|
-
* @returns {Promise<object|null>} a hookSpecificOutput loop warning, or null
|
|
32
|
-
* (pass/silent).
|
|
33
|
-
*/
|
|
34
15
|
export async function run(payload) {
|
|
35
16
|
try {
|
|
36
17
|
if (!stateExists()) return null;
|
|
@@ -43,7 +24,6 @@ export async function run(payload) {
|
|
|
43
24
|
|
|
44
25
|
mkdirSync(memoryDir, { recursive: true });
|
|
45
26
|
|
|
46
|
-
// Read existing buffer
|
|
47
27
|
let buffer = [];
|
|
48
28
|
try {
|
|
49
29
|
if (existsSync(bufferPath)) {
|
|
@@ -54,19 +34,15 @@ export async function run(payload) {
|
|
|
54
34
|
buffer = [];
|
|
55
35
|
}
|
|
56
36
|
|
|
57
|
-
// Append current action
|
|
58
37
|
const sig = getCommandSignature(payload);
|
|
59
38
|
buffer.push({ sig, ts: new Date().toISOString() });
|
|
60
39
|
|
|
61
|
-
// Trim to max size (FIFO)
|
|
62
40
|
if (buffer.length > MAX_BUFFER) {
|
|
63
41
|
buffer = buffer.slice(-MAX_BUFFER);
|
|
64
42
|
}
|
|
65
43
|
|
|
66
|
-
// Save updated buffer
|
|
67
44
|
writeFileSync(bufferPath, JSON.stringify(buffer), 'utf-8');
|
|
68
45
|
|
|
69
|
-
// Check for loop (need at least LOOP_WINDOW actions)
|
|
70
46
|
if (buffer.length < LOOP_WINDOW) return null;
|
|
71
47
|
|
|
72
48
|
const recent = buffer.slice(-LOOP_WINDOW);
|
|
@@ -74,18 +50,14 @@ export async function run(payload) {
|
|
|
74
50
|
|
|
75
51
|
if (uniqueSigs.size > MAX_UNIQUE_SIGS) return null;
|
|
76
52
|
|
|
77
|
-
// Check cooldown
|
|
78
53
|
try {
|
|
79
54
|
if (existsSync(cooldownPath)) {
|
|
80
55
|
const cooldown = JSON.parse(readFileSync(cooldownPath, 'utf-8'));
|
|
81
56
|
const elapsed = Date.now() - new Date(cooldown.lastDetected).getTime();
|
|
82
57
|
if (elapsed < COOLDOWN_MS) return null;
|
|
83
58
|
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Ignore corrupt cooldown — proceed with detection
|
|
86
|
-
}
|
|
59
|
+
} catch {}
|
|
87
60
|
|
|
88
|
-
// Loop detected — set cooldown and warn
|
|
89
61
|
writeFileSync(cooldownPath, JSON.stringify({ lastDetected: new Date().toISOString() }), 'utf-8');
|
|
90
62
|
|
|
91
63
|
logEvent('loop_detected', { signatures: uniqueSigs.size }, process.cwd());
|
|
@@ -102,7 +74,6 @@ export async function run(payload) {
|
|
|
102
74
|
'PostToolUse'
|
|
103
75
|
);
|
|
104
76
|
} catch {
|
|
105
|
-
// Fail-open
|
|
106
77
|
return null;
|
|
107
78
|
}
|
|
108
79
|
}
|
|
@@ -38,10 +38,6 @@ export function matchesExecutableRule(command, rule) {
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
|
|
43
|
-
* @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
|
|
44
|
-
*/
|
|
45
41
|
export async function run(payload) {
|
|
46
42
|
try {
|
|
47
43
|
if (!stateExists()) return null;
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* PreToolUse Hook: Protect Approved Spec Files
|
|
5
|
-
*
|
|
6
|
-
* Event: PreToolUse | Matcher: Write|Edit
|
|
7
|
-
*
|
|
8
|
-
* Blocks Write/Edit to spec artifacts after their approval gate has passed:
|
|
9
|
-
* - 2-plan/plan.md → after 'plan', only an append under `## Errata`
|
|
10
|
-
* - 2-plan/checks.json → never by hand: `morph-spec plan-check` is its only writer
|
|
11
|
-
* - 1-design/design-system.md, etc. → after 'uiux', only an errata append
|
|
12
|
-
*
|
|
13
|
-
* A gate freezes the APPROVED ARTIFACT, so a protected file that does not yet
|
|
14
|
-
* exist on disk is a CREATION and passes — the criterion is the file's
|
|
15
|
-
* existence, never who signed the gate (see the carve-out in run()).
|
|
16
|
-
*
|
|
17
|
-
* Fail-open: exits 0 on any error.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
3
|
import { existsSync, readFileSync } from 'fs';
|
|
21
4
|
import { stateExists, isGateApproved } from '../../shared/state-reader.js';
|
|
22
5
|
import {
|
|
@@ -32,10 +15,6 @@ import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
|
|
|
32
15
|
|
|
33
16
|
const CHECKS_FILE = 'checks.json';
|
|
34
17
|
|
|
35
|
-
/**
|
|
36
|
-
* Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
|
|
37
|
-
* @returns {string|null} null quando o payload não permite reconstruir (fail-open).
|
|
38
|
-
*/
|
|
39
18
|
function proposedText(payload, toolName, original) {
|
|
40
19
|
if (toolName === 'Edit') {
|
|
41
20
|
const oldStr = payload?.tool_input?.old_string;
|
|
@@ -49,14 +28,8 @@ function proposedText(payload, toolName, original) {
|
|
|
49
28
|
return typeof content === 'string' ? content : null;
|
|
50
29
|
}
|
|
51
30
|
|
|
52
|
-
/**
|
|
53
|
-
* @param {object} payload - parsed PreToolUse stdin payload
|
|
54
|
-
* @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
|
|
55
|
-
* Never prints, never exits — see runStandalone()/chain.js at the bottom.
|
|
56
|
-
*/
|
|
57
31
|
export async function run(payload) {
|
|
58
32
|
try {
|
|
59
|
-
// Amend mode: bypass spec protection for legitimate in-implementation corrections
|
|
60
33
|
if (process.env.MORPH_AMEND_PHASE) {
|
|
61
34
|
console.error(`[morph-spec] AMEND MODE: bypassing spec protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
|
|
62
35
|
return null;
|
|
@@ -67,14 +40,9 @@ export async function run(payload) {
|
|
|
67
40
|
const filePath = getFilePath(payload);
|
|
68
41
|
if (!filePath) return null;
|
|
69
42
|
|
|
70
|
-
// Only check files inside .morph/features/
|
|
71
43
|
if (!isFeaturePath(filePath)) return null;
|
|
72
44
|
|
|
73
|
-
// The gate
|
|
74
|
-
// that OWNS it. Asking the session's tree instead made this guard go DARK in
|
|
75
|
-
// a secondary worktree: the root had no approved gate, so a post-Gate-2
|
|
76
|
-
// plan.md rewrite sailed through. Same defect as enforce-phase-writes,
|
|
77
|
-
// opposite direction — there it over-blocked, here it under-protected.
|
|
45
|
+
// The gate lives in the feature.json of the tree that OWNS the file: asking the session's tree left this guard dark in a worktree.
|
|
78
46
|
const root = resolveMorphRoot(filePath, process.cwd());
|
|
79
47
|
if (!stateExists(root)) return null;
|
|
80
48
|
|
|
@@ -84,12 +52,10 @@ export async function run(payload) {
|
|
|
84
52
|
const filename = getBasename(filePath);
|
|
85
53
|
const requiredGate = PROTECTED_SPEC_FILES[filename];
|
|
86
54
|
|
|
87
|
-
// Not a protected file
|
|
88
55
|
if (!requiredGate) return null;
|
|
89
56
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// chore/hotfix are born with the plan gate stamped.
|
|
57
|
+
// Generated only by the CLI: a hand-written one would carry checks the parser never judged.
|
|
58
|
+
// Decided before the creation carve-out, since chore/hotfix are born with the plan gate stamped.
|
|
93
59
|
if (filename === CHECKS_FILE) {
|
|
94
60
|
logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
|
|
95
61
|
return blockResult(
|
|
@@ -100,53 +66,28 @@ export async function run(payload) {
|
|
|
100
66
|
);
|
|
101
67
|
}
|
|
102
68
|
|
|
103
|
-
// Gate not yet approved — allow editing
|
|
104
69
|
if (!isGateApproved(featureName, requiredGate, root)) {
|
|
105
70
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
106
71
|
return null;
|
|
107
72
|
}
|
|
108
73
|
|
|
109
|
-
// ── Gate approved — file is locked ──────────────────────────────────────
|
|
110
74
|
const absPath = resolveTargetPath(filePath, root);
|
|
111
75
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// inicial, não a edição de uma spec fechada. O caso é real e não é raro —
|
|
115
|
-
// `chore`/`hotfix` nascem com `plan` carimbado `policy:{tipo}`
|
|
116
|
-
// (create.js → work-types.js skippedGates), então o primeiro `Write` de
|
|
117
|
-
// `2-plan/plan.md` já chega aqui com o gate "aprovado" sem que humano
|
|
118
|
-
// nenhum tenha aprovado coisa alguma — e era barrado com uma mensagem
|
|
119
|
-
// mandando revogar o gate de um arquivo inexistente.
|
|
120
|
-
//
|
|
121
|
-
// O critério é a EXISTÊNCIA, nunca o `approvedBy`. Liberar por assinatura
|
|
122
|
-
// (`policy:` passa, `lucas` não) transformaria este carve-out em licença
|
|
123
|
-
// para reescrever qualquer spec aprovada: bastaria apagá-la antes. Por ser
|
|
124
|
-
// sobre o disco, a janela fecha sozinha no instante em que o arquivo
|
|
125
|
-
// existe — os contra-canários de test/hooks/protect-spec-files.test.js
|
|
126
|
-
// pinam exatamente isso.
|
|
127
|
-
// O `result` distinto no log é o que torna a criação observável.
|
|
76
|
+
// A file not yet on disk is its CREATION (chore/hotfix are born with `plan` stamped), not an edit of a frozen spec.
|
|
77
|
+
// The criterion is existence, never `approvedBy`: by signature, deleting a spec first would license rewriting it.
|
|
128
78
|
if (!existsSync(absPath)) {
|
|
129
79
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
|
|
130
80
|
return null;
|
|
131
81
|
}
|
|
132
82
|
|
|
133
|
-
//
|
|
134
|
-
// O corpo continua congelado. A ÚNICA passagem é a errata append-only:
|
|
135
|
-
// corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
|
|
136
|
-
// de leitura/reconstrução cai no bloqueio: a regra base é "não se edita".
|
|
137
|
-
//
|
|
138
|
-
// O arquivo EXISTE neste ponto — a inexistência foi decidida lá em cima, e
|
|
139
|
-
// é a única coisa que este catch NÃO significa mais. ENOENT tratado como
|
|
140
|
-
// "ilegível" era o defeito F14; "ilegível de verdade" continua bloqueando.
|
|
83
|
+
// The only way through is the append-only errata; any read or rebuild failure falls into the block.
|
|
141
84
|
const toolName = getToolName(payload);
|
|
142
85
|
let errataCheck = null;
|
|
143
86
|
try {
|
|
144
87
|
const original = readFileSync(absPath, 'utf-8');
|
|
145
88
|
const proposed = proposedText(payload, toolName, original);
|
|
146
89
|
if (proposed !== null) errataCheck = errataViolation(original, proposed);
|
|
147
|
-
} catch {
|
|
148
|
-
// arquivo ilegível → sem errata possível, segue para o bloqueio
|
|
149
|
-
}
|
|
90
|
+
} catch {}
|
|
150
91
|
|
|
151
92
|
if (errataCheck?.ok) {
|
|
152
93
|
logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
|
|
@@ -165,7 +106,6 @@ export async function run(payload) {
|
|
|
165
106
|
`edite, rode plan-check quando for o plan.md, e re-apresente o gate para aprovação.`
|
|
166
107
|
);
|
|
167
108
|
} catch {
|
|
168
|
-
// Fail-open
|
|
169
109
|
return null;
|
|
170
110
|
}
|
|
171
111
|
}
|
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* state-sync.js — PostToolUse hook
|
|
4
|
-
*
|
|
5
|
-
* Keeps .morph/state.json in sync with the agent's actions, without requiring
|
|
6
|
-
* the agent to think about state. Fail-open: any error returns {} silently.
|
|
7
|
-
*
|
|
8
|
-
* Triggers:
|
|
9
|
-
* - Write/Edit of .morph/features/{feature}/2-plan/plan.md → register feature
|
|
10
|
-
* - Write/Edit of .morph/features/{feature}/{phase}/{output} → mark output created
|
|
11
|
-
* - AskUserQuestion answered with option starting "aprovado" → advance gate
|
|
12
|
-
*/
|
|
13
2
|
|
|
14
3
|
import { existsSync } from 'fs';
|
|
15
4
|
import { join } from 'path';
|
|
@@ -27,7 +16,7 @@ import {
|
|
|
27
16
|
|
|
28
17
|
const PHASE_FOLDERS = {
|
|
29
18
|
'1-design': 'design',
|
|
30
|
-
'1-ui': 'uiux', // legacy alias
|
|
19
|
+
'1-ui': 'uiux', // legacy alias
|
|
31
20
|
'2-plan': 'plan',
|
|
32
21
|
'3-implement': 'implement',
|
|
33
22
|
'4-review': 'review',
|
|
@@ -62,8 +51,6 @@ function registerFeatureIfMissing(state, featureName) {
|
|
|
62
51
|
if (!state.features) state.features = {};
|
|
63
52
|
if (state.features[featureName]) return false;
|
|
64
53
|
|
|
65
|
-
// Canonical object-form gates (incl. optional uiux) + stamped identity
|
|
66
|
-
// (name=slug, workType, createdAt) — no anonymous shell, no boolean drift.
|
|
67
54
|
state.features[featureName] = createDefaultFeature(featureName);
|
|
68
55
|
return true;
|
|
69
56
|
}
|
|
@@ -94,9 +81,7 @@ function isApprovalAnswer(answer) {
|
|
|
94
81
|
);
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
//
|
|
98
|
-
// installed standalone and cannot import from src/). Keep in sync:
|
|
99
|
-
// plan first; uiux and review require plan.
|
|
84
|
+
// Mirror of src/core/state/gate-order.js: hooks are installed standalone and cannot import src/.
|
|
100
85
|
const GATE_PREREQ = { plan: [], uiux: ['plan'], review: ['plan'] };
|
|
101
86
|
function unmetGatePrereqs(gate, gates) {
|
|
102
87
|
const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
|
|
@@ -108,9 +93,7 @@ function handleWriteEdit(input, sessionRoot) {
|
|
|
108
93
|
const featureName = extractFeatureName(filePath);
|
|
109
94
|
if (!featureName) return false;
|
|
110
95
|
|
|
111
|
-
// Sync the tree that OWNS the written file
|
|
112
|
-
// instead registered the feature (and its outputs) in the wrong `.morph/`
|
|
113
|
-
// whenever a root-rooted session wrote into a secondary worktree.
|
|
96
|
+
// Sync the tree that OWNS the written file: a root session also writes into worktrees.
|
|
114
97
|
const cwd = resolveMorphRoot(filePath, sessionRoot);
|
|
115
98
|
if (!stateExists(cwd)) return false;
|
|
116
99
|
|
|
@@ -125,8 +108,6 @@ function handleWriteEdit(input, sessionRoot) {
|
|
|
125
108
|
}
|
|
126
109
|
|
|
127
110
|
if (changed) {
|
|
128
|
-
// Persist the touched feature to its authoritative feature.json and refresh
|
|
129
|
-
// the thin root index. The feature's state thus travels in its own folder.
|
|
130
111
|
persistFeature(featureName, state.features[featureName], cwd);
|
|
131
112
|
}
|
|
132
113
|
return true;
|
|
@@ -142,22 +123,11 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
142
123
|
const gate = gateFromQuestion(question);
|
|
143
124
|
if (!gate) continue;
|
|
144
125
|
|
|
145
|
-
//
|
|
146
|
-
// that owns it — never to "the first in_progress feature of whatever tree
|
|
147
|
-
// the session sits in", which is how a Gate 2 for one feature was stamped
|
|
148
|
-
// onto `export-hub`, leaving it with G2 approved and G1 never asked.
|
|
126
|
+
// The feature the QUESTION names, in the tree that owns it — never the session tree's first in-progress feature.
|
|
149
127
|
const located = locateFeatureForQuestion(question, sessionRoot);
|
|
150
128
|
if (!located) {
|
|
151
|
-
// Refuse to guess
|
|
152
|
-
//
|
|
153
|
-
// explicit remediation step.
|
|
154
|
-
//
|
|
155
|
-
// The remediation differs by WHY it refused. When the question names a
|
|
156
|
-
// specific slug that no known tree has registered (a hotfix gate asked
|
|
157
|
-
// before `create` ran — the case that used to fall through to "the
|
|
158
|
-
// tree's one feature"), `morph-spec approve <that-name> <gate>` is a
|
|
159
|
-
// dead end: approve.js does `getFeature()` and hard-refuses "Feature
|
|
160
|
-
// not found". Name the real fix instead of the wrong command.
|
|
129
|
+
// Refuse to guess: a gate on the wrong feature inverts gate order. For an unregistered slug
|
|
130
|
+
// `approve` is a dead end ("Feature not found"), so the remediation names `create`.
|
|
161
131
|
const allNames = allWorkTrees(sessionRoot).flatMap((r) => featureNamesIn(r));
|
|
162
132
|
const candidate = unrecognizedSlugLike(question, allNames);
|
|
163
133
|
const remediation = candidate
|
|
@@ -176,20 +146,14 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
176
146
|
const { name: featureName, root } = located;
|
|
177
147
|
if (!stateExists(root)) continue;
|
|
178
148
|
|
|
179
|
-
//
|
|
180
|
-
// view so a feature carried only by the thin index still reports its real
|
|
181
|
-
// gates (otherwise every predecessor reads as unapproved and the
|
|
182
|
-
// out-of-order warning below fires on a perfectly ordered approval).
|
|
149
|
+
// Fallback to the merged view: a feature only in the thin index would read every predecessor as unapproved.
|
|
183
150
|
const feature = readFeatureFile(featureName, root) || loadState(root)?.features?.[featureName];
|
|
184
151
|
const gates = feature?.approvalGates;
|
|
185
152
|
const existing = gates?.[gate];
|
|
186
153
|
const alreadyApproved = existing === true || existing?.approved === true;
|
|
187
154
|
if (alreadyApproved) continue;
|
|
188
155
|
|
|
189
|
-
//
|
|
190
|
-
// (never block a human in the loop), but if a predecessor gate is still
|
|
191
|
-
// pending we surface a non-blocking warning — the CLI `morph-spec approve`
|
|
192
|
-
// hard-refuses the same shape; here we alert so the operator can reconcile.
|
|
156
|
+
// Out of order still approves (never block a human in the loop); `morph-spec approve` refuses the same shape.
|
|
193
157
|
const unmet = unmetGatePrereqs(gate, gates);
|
|
194
158
|
if (unmet.length) {
|
|
195
159
|
warnings.push(
|
|
@@ -199,13 +163,7 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
199
163
|
);
|
|
200
164
|
}
|
|
201
165
|
|
|
202
|
-
//
|
|
203
|
-
// Route through the shared canonical gate writer so the hook and the CLI
|
|
204
|
-
// (`morph-spec approve`) produce one consistent shape
|
|
205
|
-
// ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
|
|
206
|
-
// and a manual approval stays distinguishable from a policy auto-approval.
|
|
207
|
-
// Same contract as `approve`'s --self-assessed: a human may ship Gate 3 without the
|
|
208
|
-
// independent review reports, but the degradation is stamped, never silent.
|
|
166
|
+
// Same contract as `approve --self-assessed`: approving review without its reports is stamped, never silent.
|
|
209
167
|
const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
|
|
210
168
|
&& ['verifier.md', 'reviewer.md'].some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
|
|
211
169
|
if (selfAssessed) {
|
|
@@ -219,19 +177,9 @@ function handleAskUserQuestion(input, sessionRoot) {
|
|
|
219
177
|
return warnings.length ? warnings.join('\n') : null;
|
|
220
178
|
}
|
|
221
179
|
|
|
222
|
-
/**
|
|
223
|
-
* @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
|
|
224
|
-
* @returns {Promise<object|null>} a hookSpecificOutput warning on a gate
|
|
225
|
-
* order/attribution violation, or null (pass/silent).
|
|
226
|
-
*/
|
|
227
180
|
export async function run(payload) {
|
|
228
181
|
try {
|
|
229
|
-
//
|
|
230
|
-
// gets read and written is decided per event — by the target path for
|
|
231
|
-
// Write/Edit, by the feature the question names for AskUserQuestion. The
|
|
232
|
-
// `stateExists` gate therefore moved into each handler, against the tree that
|
|
233
|
-
// handler resolved; gating on the session's tree up here would skip work that
|
|
234
|
-
// belongs to a worktree (and, worse, silently pass it to the wrong one).
|
|
182
|
+
// Only a starting point: each handler resolves the tree it reads and writes, so the stateExists gate lives there.
|
|
235
183
|
const sessionRoot = projectDir();
|
|
236
184
|
|
|
237
185
|
const input = payload || {};
|
|
@@ -244,12 +192,8 @@ export async function run(payload) {
|
|
|
244
192
|
} else if (toolName === 'AskUserQuestion') {
|
|
245
193
|
warning = handleAskUserQuestion(input, sessionRoot);
|
|
246
194
|
}
|
|
247
|
-
} catch {
|
|
248
|
-
// fail-open
|
|
249
|
-
}
|
|
195
|
+
} catch {}
|
|
250
196
|
|
|
251
|
-
// A non-blocking order-violation warning is surfaced as injected context
|
|
252
|
-
// (hookSpecificOutput.additionalContext); everything else stays silent.
|
|
253
197
|
return warning ? contextResult(warning, 'PostToolUse') : null;
|
|
254
198
|
} catch {
|
|
255
199
|
return null;
|
|
@@ -1,41 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Telemetry Hook: harness execution-path capture
|
|
5
|
-
*
|
|
6
|
-
* Events: PostToolUse + PostToolUseFailure (all tools), PostToolBatch,
|
|
7
|
-
* SubagentStop, UserPromptSubmit
|
|
8
|
-
*
|
|
9
|
-
* morph-spec is a Natural Language Harness: there is no central runtime to
|
|
10
|
-
* instrument — Claude Code executes the skills and sub-agents. The only
|
|
11
|
-
* vantage points are hooks. This one is the generic capture point: every tool
|
|
12
|
-
* call and every user prompt is appended (one JSON line) to
|
|
13
|
-
* .morph/logs/events.jsonl, from which `morph-spec telemetry` and
|
|
14
|
-
* `morph-spec cost` reconstruct the path and measure it.
|
|
15
|
-
*
|
|
16
|
-
* Correlation is by an EXPLICIT key (feature + phase) written into each event,
|
|
17
|
-
* not by ambient span context — hooks fire as separate node processes with no
|
|
18
|
-
* shared trace context. `sessionId` and `agentId` (present only inside a
|
|
19
|
-
* sub-agent) separate the orchestrator from the sub-agents it dispatched.
|
|
20
|
-
*
|
|
21
|
-
* PostToolUse fires only when a tool SUCCEEDS; a Bash command that exits
|
|
22
|
-
* non-zero fires PostToolUseFailure instead. Only the exit code parsed from
|
|
23
|
-
* its `error` first line is kept — the rest of `error` is the command's
|
|
24
|
-
* output, where a secret value can appear with no name beside it.
|
|
25
|
-
*
|
|
26
|
-
* SINK RESOLUTION: the event is appended to the `.morph/` tree that OWNS the
|
|
27
|
-
* tool's target file — the tree of its `.morph/features/` segment
|
|
28
|
-
* (`resolveMorphRoot`), else the nearest morph tree above the file; tools with
|
|
29
|
-
* no file path sink on the nearest morph tree above the payload's `cwd`, so a
|
|
30
|
-
* sub-agent working in a worktree logs its Bash calls next to its Reads and
|
|
31
|
-
* Writes. The feature is the tree's BINDING (`morph/<f>` branch, then
|
|
32
|
-
* `MORPH_FEATURE` in `.morph/worktree.env`) before its most recent active
|
|
33
|
-
* feature; a bound feature with no `feature.json` in that tree keeps phase null.
|
|
34
|
-
*
|
|
35
|
-
* SILENT: never blocks, never injects context — only appends. Fail-open on any
|
|
36
|
-
* error (exit 0). Only active inside a morph project (stateExists gate).
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
3
|
import { statSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
40
4
|
import { dirname, join } from 'path';
|
|
41
5
|
import { stateExists, getActiveFeature, derivePhaseForFeature } from '../../shared/state-reader.js';
|
|
@@ -51,10 +15,7 @@ function normalize(p) {
|
|
|
51
15
|
return (p || '').replace(/\\/g, '/');
|
|
52
16
|
}
|
|
53
17
|
|
|
54
|
-
//
|
|
55
|
-
// per-line cost. The ORIGINAL length travels in a sibling `<field>Chars` key,
|
|
56
|
-
// which `morph-spec cost` prefers over the truncated string's own length —
|
|
57
|
-
// a contract with the telemetry reader: do not rename either half.
|
|
18
|
+
// The original length travels in a sibling `<field>Chars` key that `morph-spec cost` prefers: do not rename either half.
|
|
58
19
|
const MAX_FIELD_CHARS = 2000;
|
|
59
20
|
const TRUNCATE_SUFFIX = '…[truncado]';
|
|
60
21
|
|
|
@@ -64,13 +25,7 @@ const DEFAULT_READ_LINES = 2000;
|
|
|
64
25
|
const MAX_SLICE_BYTES = 10 * 1024 * 1024;
|
|
65
26
|
const BINARY_READ = /\.(png|jpe?g|gif|webp|bmp|ico|pdf)$/i;
|
|
66
27
|
|
|
67
|
-
|
|
68
|
-
* Redact, then cap a free-text field, reporting its original length. Redacting
|
|
69
|
-
* first keeps a token cut at the cap from surviving as a recognizable prefix.
|
|
70
|
-
*
|
|
71
|
-
* @param {string} value
|
|
72
|
-
* @returns {{ value: string, chars: number }}
|
|
73
|
-
*/
|
|
28
|
+
// Redact before capping: a token cut at the cap would survive as a recognizable prefix.
|
|
74
29
|
function capField(value) {
|
|
75
30
|
const str = typeof value === 'string' ? value : '';
|
|
76
31
|
const clean = redactSecrets(str);
|
|
@@ -78,15 +33,11 @@ function capField(value) {
|
|
|
78
33
|
return { value: clean.slice(0, MAX_FIELD_CHARS) + TRUNCATE_SUFFIX, chars: str.length };
|
|
79
34
|
}
|
|
80
35
|
|
|
81
|
-
|
|
82
|
-
* Classify a Read target into the telemetry `kind` taxonomy — a contract with
|
|
83
|
-
* src/lib/telemetry/telemetry-reader.js, so these values are pinned. Specific
|
|
84
|
-
* categories first, 'code' last.
|
|
85
|
-
*/
|
|
36
|
+
// The `kind` values are a contract with src/lib/telemetry/telemetry-reader.js.
|
|
86
37
|
function classifyReadKind(target) {
|
|
87
38
|
const t = (target || '').toLowerCase();
|
|
88
39
|
|
|
89
|
-
if (t.includes('evals/review-checklist')) return '
|
|
40
|
+
if (t.includes('evals/review-checklist')) return 'checklist';
|
|
90
41
|
if (t.includes('standards/')) return 'standard';
|
|
91
42
|
if (t.endsWith('skill.md') || t.includes('/skills/')) return 'skill';
|
|
92
43
|
if (t.split('/').pop() === 'agents.json') return 'agents';
|
|
@@ -107,7 +58,6 @@ function classifyReadKind(target) {
|
|
|
107
58
|
return 'code';
|
|
108
59
|
}
|
|
109
60
|
|
|
110
|
-
/** Nearest directory at or above `dir` that is a morph project; `fallback` when none. */
|
|
111
61
|
function morphRootAbove(dir, fallback) {
|
|
112
62
|
if (!dir) return fallback;
|
|
113
63
|
let current = normalize(dir);
|
|
@@ -122,12 +72,7 @@ function morphRootAbove(dir, fallback) {
|
|
|
122
72
|
|
|
123
73
|
const FEATURE_NAME = /^[A-Za-z0-9._-]+$/;
|
|
124
74
|
|
|
125
|
-
|
|
126
|
-
* The feature the TREE is bound to, before any guess from its state: the
|
|
127
|
-
* branch `morph/<f>` checked out there, then `MORPH_FEATURE` in its
|
|
128
|
-
* `.morph/worktree.env` (a task worktree sits on `morph-task/<n>`). Without
|
|
129
|
-
* it, a sub-agent's worktree reported the root's most recent active feature.
|
|
130
|
-
*/
|
|
75
|
+
// Branch binding before state: otherwise a sub-agent's worktree reports the root's most recent active feature.
|
|
131
76
|
async function boundFeature(root) {
|
|
132
77
|
const fromBranch = morphBranchFeature(root);
|
|
133
78
|
if (fromBranch) return fromBranch;
|
|
@@ -143,10 +88,6 @@ function featureFromPath(target) {
|
|
|
143
88
|
return m ? m[1] : null;
|
|
144
89
|
}
|
|
145
90
|
|
|
146
|
-
/**
|
|
147
|
-
* What a Read actually put in context: the requested line window, not the
|
|
148
|
-
* whole file. Binary reads (images, PDFs) go in whole.
|
|
149
|
-
*/
|
|
150
91
|
function readSliceBytes(absPath, input, size) {
|
|
151
92
|
if (BINARY_READ.test(absPath)) return size;
|
|
152
93
|
if (size > MAX_SLICE_BYTES) return undefined;
|
|
@@ -157,7 +98,7 @@ function readSliceBytes(absPath, input, size) {
|
|
|
157
98
|
return Buffer.byteLength(lines.slice(offset - 1, offset - 1 + limit).join('\n'), 'utf-8');
|
|
158
99
|
}
|
|
159
100
|
|
|
160
|
-
|
|
101
|
+
// Only the exit code is kept: the rest of `error` is command output, where a secret can appear with no name beside it.
|
|
161
102
|
function outcomeOf(payload, input) {
|
|
162
103
|
if (payload.hook_event_name === 'PostToolUseFailure') {
|
|
163
104
|
const firstLine = String(payload.error ?? '').split('\n', 1)[0];
|
|
@@ -181,11 +122,7 @@ function countByName(calls) {
|
|
|
181
122
|
return out;
|
|
182
123
|
}
|
|
183
124
|
|
|
184
|
-
|
|
185
|
-
* Sub-agent tokens live in their own transcript; each stop emits the delta.
|
|
186
|
-
* One file per agent: two sub-agents stopping together never share a
|
|
187
|
-
* read-modify-write, and one agent never stops twice at once.
|
|
188
|
-
*/
|
|
125
|
+
// One store file per agent, so two sub-agents stopping together never share a read-modify-write.
|
|
189
126
|
function agentUsageEvent(payload, cwd) {
|
|
190
127
|
const usage = readTranscriptUsage(payload.agent_transcript_path);
|
|
191
128
|
if (!usage || !payload.agent_id) return null;
|
|
@@ -204,11 +141,6 @@ function agentUsageEvent(payload, cwd) {
|
|
|
204
141
|
return { event: 'AgentUsage', ...usage, delta };
|
|
205
142
|
}
|
|
206
143
|
|
|
207
|
-
/**
|
|
208
|
-
* @param {object} payload - parsed hook payload (PostToolUse, PostToolUseFailure,
|
|
209
|
-
* PostToolBatch, SubagentStop or UserPromptSubmit)
|
|
210
|
-
* @returns {Promise<null>} always null — pure side effect (append + claim heartbeat)
|
|
211
|
-
*/
|
|
212
144
|
export async function run(payload) {
|
|
213
145
|
try {
|
|
214
146
|
if (!payload) return null;
|
|
@@ -222,8 +154,6 @@ export async function run(payload) {
|
|
|
222
154
|
|
|
223
155
|
if (!stateExists(cwd)) return null;
|
|
224
156
|
|
|
225
|
-
// getActiveFeature → loadState hydrates every feature.json: O(active
|
|
226
|
-
// features) per call, fine while archive keeps that set small.
|
|
227
157
|
let feature = null;
|
|
228
158
|
let phase = null;
|
|
229
159
|
try {
|
|
@@ -237,14 +167,9 @@ export async function run(payload) {
|
|
|
237
167
|
feature = active?.name ?? featureFromPath(targetFilePath);
|
|
238
168
|
if (feature) phase = active?.feature?.phase || derivePhaseForFeature(feature, cwd);
|
|
239
169
|
}
|
|
240
|
-
} catch {
|
|
241
|
-
// enrichment is best-effort — a missing feature never blocks capture
|
|
242
|
-
}
|
|
170
|
+
} catch {}
|
|
243
171
|
|
|
244
|
-
// Claim heartbeat:
|
|
245
|
-
// of life for the advisory feature claim. shouldHeartbeat() is a local,
|
|
246
|
-
// git-free pre-check that says no on ~99% of calls; only then is the git
|
|
247
|
-
// common dir resolved (pure filesystem, no subprocess).
|
|
172
|
+
// Claim heartbeat: shouldHeartbeat is a git-free pre-check that says no on ~99% of calls.
|
|
248
173
|
if (feature && payload.session_id) {
|
|
249
174
|
try {
|
|
250
175
|
const { shouldHeartbeat, heartbeatClaim } = await import('../../shared/claim-registry.js');
|
|
@@ -253,7 +178,7 @@ export async function run(payload) {
|
|
|
253
178
|
const common = resolveGitCommonDir(cwd);
|
|
254
179
|
if (common) heartbeatClaim(feature, { gitCommonDir: common, sessionId: payload.session_id });
|
|
255
180
|
}
|
|
256
|
-
} catch {
|
|
181
|
+
} catch {}
|
|
257
182
|
}
|
|
258
183
|
|
|
259
184
|
const hookEvent = payload.hook_event_name;
|
|
@@ -290,17 +215,14 @@ export async function run(payload) {
|
|
|
290
215
|
event.kind = classifyReadKind(event.target);
|
|
291
216
|
if (Number.isInteger(input.offset)) event.offset = input.offset;
|
|
292
217
|
if (Number.isInteger(input.limit)) event.limit = input.limit;
|
|
293
|
-
// A failed Read ingested nothing
|
|
294
|
-
// between the Read and this hook omits the numbers rather than fail.
|
|
218
|
+
// A failed Read ingested nothing; a file deleted before this hook just omits the numbers.
|
|
295
219
|
if (hookEvent !== 'PostToolUseFailure') {
|
|
296
220
|
try {
|
|
297
221
|
const abs = targetAbs || resolveTargetPath(event.target, projectDir);
|
|
298
222
|
event.bytes = statSync(abs).size;
|
|
299
223
|
const read = readSliceBytes(abs, input, event.bytes);
|
|
300
224
|
if (read !== undefined) event.bytesRead = read;
|
|
301
|
-
} catch {
|
|
302
|
-
// omit bytes
|
|
303
|
-
}
|
|
225
|
+
} catch {}
|
|
304
226
|
}
|
|
305
227
|
} else if (tool === 'Bash' || tool === 'PowerShell') {
|
|
306
228
|
const command = capField(input.command || '');
|
|
@@ -311,8 +233,6 @@ export async function run(payload) {
|
|
|
311
233
|
} else if (tool === 'Write' || tool === 'Edit') {
|
|
312
234
|
event.target = normalize(input.file_path || input.path);
|
|
313
235
|
} else {
|
|
314
|
-
// Every other tool still counts toward "the whole path" — a compact
|
|
315
|
-
// target keeps Grep/Glob/WebFetch bursts distinguishable.
|
|
316
236
|
const target = input.file_path || input.path || input.pattern || input.query || input.url || '';
|
|
317
237
|
if (target) event.target = normalize(String(target));
|
|
318
238
|
}
|
|
@@ -333,7 +253,6 @@ export async function run(payload) {
|
|
|
333
253
|
appendTelemetryEvent(event, cwd);
|
|
334
254
|
return null;
|
|
335
255
|
} catch {
|
|
336
|
-
// Fail-open
|
|
337
256
|
return null;
|
|
338
257
|
}
|
|
339
258
|
}
|