cap-pro 1.0.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-plugin/README.md +26 -0
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.ja-JP.md +834 -0
- package/README.ko-KR.md +823 -0
- package/README.md +806 -0
- package/README.pt-BR.md +452 -0
- package/README.zh-CN.md +800 -0
- package/agents/cap-architect.md +269 -0
- package/agents/cap-brainstormer.md +207 -0
- package/agents/cap-curator.md +276 -0
- package/agents/cap-debugger.md +365 -0
- package/agents/cap-designer.md +246 -0
- package/agents/cap-historian.md +464 -0
- package/agents/cap-migrator.md +291 -0
- package/agents/cap-prototyper.md +197 -0
- package/agents/cap-validator.md +308 -0
- package/bin/install.js +5433 -0
- package/cap/bin/cap-tools.cjs +853 -0
- package/cap/bin/lib/arc-scanner.cjs +344 -0
- package/cap/bin/lib/cap-affinity-engine.cjs +862 -0
- package/cap/bin/lib/cap-anchor.cjs +228 -0
- package/cap/bin/lib/cap-annotation-writer.cjs +340 -0
- package/cap/bin/lib/cap-checkpoint.cjs +434 -0
- package/cap/bin/lib/cap-cluster-detect.cjs +945 -0
- package/cap/bin/lib/cap-cluster-display.cjs +52 -0
- package/cap/bin/lib/cap-cluster-format.cjs +245 -0
- package/cap/bin/lib/cap-cluster-helpers.cjs +295 -0
- package/cap/bin/lib/cap-cluster-io.cjs +212 -0
- package/cap/bin/lib/cap-completeness.cjs +540 -0
- package/cap/bin/lib/cap-deps.cjs +583 -0
- package/cap/bin/lib/cap-design-families.cjs +332 -0
- package/cap/bin/lib/cap-design.cjs +966 -0
- package/cap/bin/lib/cap-divergence-detector.cjs +400 -0
- package/cap/bin/lib/cap-doctor.cjs +752 -0
- package/cap/bin/lib/cap-feature-map-internals.cjs +19 -0
- package/cap/bin/lib/cap-feature-map-migrate.cjs +335 -0
- package/cap/bin/lib/cap-feature-map-monorepo.cjs +885 -0
- package/cap/bin/lib/cap-feature-map-shard.cjs +315 -0
- package/cap/bin/lib/cap-feature-map.cjs +1943 -0
- package/cap/bin/lib/cap-fitness-score.cjs +1075 -0
- package/cap/bin/lib/cap-impact-analysis.cjs +652 -0
- package/cap/bin/lib/cap-learn-review.cjs +1072 -0
- package/cap/bin/lib/cap-learning-signals.cjs +627 -0
- package/cap/bin/lib/cap-loader.cjs +227 -0
- package/cap/bin/lib/cap-logger.cjs +57 -0
- package/cap/bin/lib/cap-memory-bridge.cjs +764 -0
- package/cap/bin/lib/cap-memory-confidence.cjs +452 -0
- package/cap/bin/lib/cap-memory-dir.cjs +987 -0
- package/cap/bin/lib/cap-memory-engine.cjs +698 -0
- package/cap/bin/lib/cap-memory-extends.cjs +398 -0
- package/cap/bin/lib/cap-memory-graph.cjs +790 -0
- package/cap/bin/lib/cap-memory-migrate.cjs +2015 -0
- package/cap/bin/lib/cap-memory-pin.cjs +183 -0
- package/cap/bin/lib/cap-memory-platform.cjs +490 -0
- package/cap/bin/lib/cap-memory-prune.cjs +707 -0
- package/cap/bin/lib/cap-memory-schema.cjs +812 -0
- package/cap/bin/lib/cap-migrate-tags.cjs +309 -0
- package/cap/bin/lib/cap-migrate.cjs +540 -0
- package/cap/bin/lib/cap-pattern-apply.cjs +1203 -0
- package/cap/bin/lib/cap-pattern-pipeline.cjs +1034 -0
- package/cap/bin/lib/cap-plugin-manifest.cjs +80 -0
- package/cap/bin/lib/cap-realtime-affinity.cjs +399 -0
- package/cap/bin/lib/cap-reconcile.cjs +570 -0
- package/cap/bin/lib/cap-research-gate.cjs +218 -0
- package/cap/bin/lib/cap-scope-filter.cjs +402 -0
- package/cap/bin/lib/cap-semantic-pipeline.cjs +1038 -0
- package/cap/bin/lib/cap-session-extract.cjs +987 -0
- package/cap/bin/lib/cap-session.cjs +445 -0
- package/cap/bin/lib/cap-snapshot-linkage.cjs +963 -0
- package/cap/bin/lib/cap-stack-docs.cjs +646 -0
- package/cap/bin/lib/cap-tag-observer.cjs +371 -0
- package/cap/bin/lib/cap-tag-scanner.cjs +1766 -0
- package/cap/bin/lib/cap-telemetry.cjs +466 -0
- package/cap/bin/lib/cap-test-audit.cjs +1438 -0
- package/cap/bin/lib/cap-thread-migrator.cjs +307 -0
- package/cap/bin/lib/cap-thread-synthesis.cjs +545 -0
- package/cap/bin/lib/cap-thread-tracker.cjs +519 -0
- package/cap/bin/lib/cap-trace.cjs +399 -0
- package/cap/bin/lib/cap-trust-mode.cjs +336 -0
- package/cap/bin/lib/cap-ui-design-editor.cjs +642 -0
- package/cap/bin/lib/cap-ui-mind-map.cjs +712 -0
- package/cap/bin/lib/cap-ui-thread-nav.cjs +693 -0
- package/cap/bin/lib/cap-ui.cjs +1245 -0
- package/cap/bin/lib/cap-upgrade.cjs +1028 -0
- package/cap/bin/lib/cli/arg-helpers.cjs +49 -0
- package/cap/bin/lib/cli/frontmatter-router.cjs +31 -0
- package/cap/bin/lib/cli/init-router.cjs +68 -0
- package/cap/bin/lib/cli/phase-router.cjs +102 -0
- package/cap/bin/lib/cli/state-router.cjs +61 -0
- package/cap/bin/lib/cli/template-router.cjs +37 -0
- package/cap/bin/lib/cli/uat-router.cjs +29 -0
- package/cap/bin/lib/cli/validation-router.cjs +26 -0
- package/cap/bin/lib/cli/verification-router.cjs +31 -0
- package/cap/bin/lib/cli/workstream-router.cjs +39 -0
- package/cap/bin/lib/commands.cjs +961 -0
- package/cap/bin/lib/config.cjs +467 -0
- package/cap/bin/lib/convention-reader.cjs +258 -0
- package/cap/bin/lib/core.cjs +1241 -0
- package/cap/bin/lib/feature-aggregator.cjs +423 -0
- package/cap/bin/lib/frontmatter.cjs +337 -0
- package/cap/bin/lib/init.cjs +1443 -0
- package/cap/bin/lib/manifest-generator.cjs +383 -0
- package/cap/bin/lib/milestone.cjs +253 -0
- package/cap/bin/lib/model-profiles.cjs +69 -0
- package/cap/bin/lib/monorepo-context.cjs +226 -0
- package/cap/bin/lib/monorepo-migrator.cjs +509 -0
- package/cap/bin/lib/phase.cjs +889 -0
- package/cap/bin/lib/profile-output.cjs +989 -0
- package/cap/bin/lib/profile-pipeline.cjs +540 -0
- package/cap/bin/lib/roadmap.cjs +330 -0
- package/cap/bin/lib/security.cjs +394 -0
- package/cap/bin/lib/session-manager.cjs +292 -0
- package/cap/bin/lib/skeleton-generator.cjs +179 -0
- package/cap/bin/lib/state.cjs +1032 -0
- package/cap/bin/lib/template.cjs +231 -0
- package/cap/bin/lib/test-detector.cjs +62 -0
- package/cap/bin/lib/uat.cjs +283 -0
- package/cap/bin/lib/verify.cjs +889 -0
- package/cap/bin/lib/workspace-detector.cjs +371 -0
- package/cap/bin/lib/workstream.cjs +492 -0
- package/cap/commands/gsd/workstreams.md +63 -0
- package/cap/references/arc-standard.md +315 -0
- package/cap/references/cap-agent-architecture.md +101 -0
- package/cap/references/cap-gitignore-template +9 -0
- package/cap/references/cap-zero-deps.md +158 -0
- package/cap/references/checkpoints.md +778 -0
- package/cap/references/continuation-format.md +249 -0
- package/cap/references/contract-test-templates.md +312 -0
- package/cap/references/feature-map-template.md +25 -0
- package/cap/references/git-integration.md +295 -0
- package/cap/references/git-planning-commit.md +38 -0
- package/cap/references/model-profiles.md +174 -0
- package/cap/references/phase-numbering.md +126 -0
- package/cap/references/planning-config.md +202 -0
- package/cap/references/property-test-templates.md +316 -0
- package/cap/references/security-test-templates.md +347 -0
- package/cap/references/session-template.json +8 -0
- package/cap/references/tdd.md +263 -0
- package/cap/references/user-profiling.md +681 -0
- package/cap/references/verification-patterns.md +612 -0
- package/cap/templates/UAT.md +265 -0
- package/cap/templates/claude-md.md +175 -0
- package/cap/templates/codebase/architecture.md +255 -0
- package/cap/templates/codebase/concerns.md +310 -0
- package/cap/templates/codebase/conventions.md +307 -0
- package/cap/templates/codebase/integrations.md +280 -0
- package/cap/templates/codebase/stack.md +186 -0
- package/cap/templates/codebase/structure.md +285 -0
- package/cap/templates/codebase/testing.md +480 -0
- package/cap/templates/config.json +44 -0
- package/cap/templates/context.md +352 -0
- package/cap/templates/continue-here.md +78 -0
- package/cap/templates/copilot-instructions.md +7 -0
- package/cap/templates/debug-subagent-prompt.md +91 -0
- package/cap/templates/discussion-log.md +63 -0
- package/cap/templates/milestone-archive.md +123 -0
- package/cap/templates/milestone.md +115 -0
- package/cap/templates/phase-prompt.md +610 -0
- package/cap/templates/planner-subagent-prompt.md +117 -0
- package/cap/templates/project.md +186 -0
- package/cap/templates/requirements.md +231 -0
- package/cap/templates/research-project/ARCHITECTURE.md +204 -0
- package/cap/templates/research-project/FEATURES.md +147 -0
- package/cap/templates/research-project/PITFALLS.md +200 -0
- package/cap/templates/research-project/STACK.md +120 -0
- package/cap/templates/research-project/SUMMARY.md +170 -0
- package/cap/templates/research.md +552 -0
- package/cap/templates/roadmap.md +202 -0
- package/cap/templates/state.md +176 -0
- package/cap/templates/summary.md +364 -0
- package/cap/templates/user-preferences.md +498 -0
- package/cap/templates/verification-report.md +322 -0
- package/cap/workflows/add-phase.md +112 -0
- package/cap/workflows/add-tests.md +351 -0
- package/cap/workflows/add-todo.md +158 -0
- package/cap/workflows/audit-milestone.md +340 -0
- package/cap/workflows/audit-uat.md +109 -0
- package/cap/workflows/autonomous.md +891 -0
- package/cap/workflows/check-todos.md +177 -0
- package/cap/workflows/cleanup.md +152 -0
- package/cap/workflows/complete-milestone.md +767 -0
- package/cap/workflows/diagnose-issues.md +231 -0
- package/cap/workflows/discovery-phase.md +289 -0
- package/cap/workflows/discuss-phase-assumptions.md +653 -0
- package/cap/workflows/discuss-phase.md +1049 -0
- package/cap/workflows/do.md +104 -0
- package/cap/workflows/execute-phase.md +846 -0
- package/cap/workflows/execute-plan.md +514 -0
- package/cap/workflows/fast.md +105 -0
- package/cap/workflows/forensics.md +265 -0
- package/cap/workflows/health.md +181 -0
- package/cap/workflows/help.md +660 -0
- package/cap/workflows/insert-phase.md +130 -0
- package/cap/workflows/list-phase-assumptions.md +178 -0
- package/cap/workflows/list-workspaces.md +56 -0
- package/cap/workflows/manager.md +362 -0
- package/cap/workflows/map-codebase.md +377 -0
- package/cap/workflows/milestone-summary.md +223 -0
- package/cap/workflows/new-milestone.md +486 -0
- package/cap/workflows/new-project.md +1250 -0
- package/cap/workflows/new-workspace.md +237 -0
- package/cap/workflows/next.md +97 -0
- package/cap/workflows/node-repair.md +92 -0
- package/cap/workflows/note.md +156 -0
- package/cap/workflows/pause-work.md +176 -0
- package/cap/workflows/plan-milestone-gaps.md +273 -0
- package/cap/workflows/plan-phase.md +857 -0
- package/cap/workflows/plant-seed.md +169 -0
- package/cap/workflows/pr-branch.md +129 -0
- package/cap/workflows/profile-user.md +449 -0
- package/cap/workflows/progress.md +507 -0
- package/cap/workflows/quick.md +757 -0
- package/cap/workflows/remove-phase.md +155 -0
- package/cap/workflows/remove-workspace.md +90 -0
- package/cap/workflows/research-phase.md +82 -0
- package/cap/workflows/resume-project.md +326 -0
- package/cap/workflows/review.md +228 -0
- package/cap/workflows/session-report.md +146 -0
- package/cap/workflows/settings.md +283 -0
- package/cap/workflows/ship.md +228 -0
- package/cap/workflows/stats.md +60 -0
- package/cap/workflows/transition.md +671 -0
- package/cap/workflows/ui-phase.md +298 -0
- package/cap/workflows/ui-review.md +161 -0
- package/cap/workflows/update.md +323 -0
- package/cap/workflows/validate-phase.md +170 -0
- package/cap/workflows/verify-phase.md +254 -0
- package/cap/workflows/verify-work.md +637 -0
- package/commands/cap/annotate.md +165 -0
- package/commands/cap/brainstorm.md +393 -0
- package/commands/cap/checkpoint.md +106 -0
- package/commands/cap/completeness.md +94 -0
- package/commands/cap/continue.md +72 -0
- package/commands/cap/debug.md +588 -0
- package/commands/cap/deps.md +169 -0
- package/commands/cap/design.md +479 -0
- package/commands/cap/init.md +354 -0
- package/commands/cap/iterate.md +249 -0
- package/commands/cap/learn.md +459 -0
- package/commands/cap/memory.md +275 -0
- package/commands/cap/migrate-feature-map.md +91 -0
- package/commands/cap/migrate-memory.md +108 -0
- package/commands/cap/migrate-tags.md +91 -0
- package/commands/cap/migrate.md +131 -0
- package/commands/cap/prototype.md +510 -0
- package/commands/cap/reconcile.md +121 -0
- package/commands/cap/review.md +360 -0
- package/commands/cap/save.md +72 -0
- package/commands/cap/scan.md +404 -0
- package/commands/cap/start.md +356 -0
- package/commands/cap/status.md +118 -0
- package/commands/cap/test-audit.md +262 -0
- package/commands/cap/test.md +394 -0
- package/commands/cap/trace.md +133 -0
- package/commands/cap/ui.md +167 -0
- package/hooks/dist/cap-check-update.js +115 -0
- package/hooks/dist/cap-context-monitor.js +185 -0
- package/hooks/dist/cap-learn-review-hook.js +114 -0
- package/hooks/dist/cap-learning-hook.js +192 -0
- package/hooks/dist/cap-memory.js +299 -0
- package/hooks/dist/cap-prompt-guard.js +97 -0
- package/hooks/dist/cap-statusline.js +157 -0
- package/hooks/dist/cap-tag-observer.js +115 -0
- package/hooks/dist/cap-version-check.js +112 -0
- package/hooks/dist/cap-workflow-guard.js +175 -0
- package/hooks/hooks.json +55 -0
- package/package.json +58 -0
- package/scripts/base64-scan.sh +262 -0
- package/scripts/build-hooks.js +93 -0
- package/scripts/cap-removal-checklist.md +202 -0
- package/scripts/prompt-injection-scan.sh +199 -0
- package/scripts/run-tests.cjs +181 -0
- package/scripts/secret-scan.sh +227 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @cap-feature(feature:F-073) Learn-Review Stop-Hook — flags pending pattern reviews after each session.
|
|
3
|
+
// Fires AFTER cap-memory's Stop hook (memory pipeline → learn pipeline →
|
|
4
|
+
// review board). cap-hook-version: {{CAP_VERSION}}
|
|
5
|
+
//
|
|
6
|
+
// Stop hook: at session end, compute shouldShowBoard(projectRoot). If the gate is met, write
|
|
7
|
+
// .cap/learning/board-pending.flag so /cap:status (and the next /cap:learn review run) can surface
|
|
8
|
+
// the pending review. We DO NOT spawn the skill from the hook — Claude Code hook subprocesses can't
|
|
9
|
+
// drive an interactive review flow.
|
|
10
|
+
//
|
|
11
|
+
// Skip via CAP_SKIP_LEARN_REVIEW_HOOK=1.
|
|
12
|
+
// Never exits non-zero: a failure here must not block the user's session end.
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
|
|
20
|
+
// @cap-todo(ac:F-073/AC-3) Skip switch — keep parity with cap-learning-hook.js / cap-memory.js so
|
|
21
|
+
// ops can disable a single learning hook without touching the others.
|
|
22
|
+
if (process.env.CAP_SKIP_LEARN_REVIEW_HOOK === '1') {
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let input = '';
|
|
27
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
28
|
+
process.stdin.setEncoding('utf8');
|
|
29
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
30
|
+
process.stdin.on('end', () => {
|
|
31
|
+
clearTimeout(stdinTimeout);
|
|
32
|
+
run(input);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function tryRequire(modulePath) {
|
|
36
|
+
try { return require(modulePath); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// @cap-decision(F-073/D1) Lib resolution mirrors cap-learning-hook.js exactly: env override →
|
|
40
|
+
// colocated in-tree → installed under ~/.claude. Keeps every CAP hook on the same
|
|
41
|
+
// resolution path so an ops change to one hook applies to the others for free.
|
|
42
|
+
function resolveReviewModule() {
|
|
43
|
+
const candidates = [];
|
|
44
|
+
if (process.env.CAP_LEARN_REVIEW_LIB) candidates.push(process.env.CAP_LEARN_REVIEW_LIB);
|
|
45
|
+
candidates.push(path.join(__dirname, '..', 'cap', 'bin', 'lib', 'cap-learn-review.cjs'));
|
|
46
|
+
candidates.push(path.join(os.homedir(), '.claude', 'cap', 'bin', 'lib', 'cap-learn-review.cjs'));
|
|
47
|
+
for (const p of candidates) {
|
|
48
|
+
const mod = tryRequire(p);
|
|
49
|
+
if (mod) return mod;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function run(raw) {
|
|
55
|
+
// @cap-todo(ac:F-073/AC-3) Whole hook body wrapped in try/catch; failures never escape.
|
|
56
|
+
try {
|
|
57
|
+
const data = raw ? JSON.parse(raw) : {};
|
|
58
|
+
const cwd = data.cwd || process.cwd();
|
|
59
|
+
|
|
60
|
+
const review = resolveReviewModule();
|
|
61
|
+
if (!review) process.exit(0); // library not installed — silent no-op (mirrors cap-memory.js).
|
|
62
|
+
|
|
63
|
+
// Compute the gate. shouldShowBoard reads SESSION.json internally for the per-session skip/reject
|
|
64
|
+
// sets — no need to pass anything explicit here.
|
|
65
|
+
let shouldShow = false;
|
|
66
|
+
try {
|
|
67
|
+
shouldShow = review.shouldShowBoard(cwd) === true;
|
|
68
|
+
} catch (_e) {
|
|
69
|
+
shouldShow = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!shouldShow) {
|
|
73
|
+
// Below threshold — NO flag. If a stale flag from a prior session is on disk we leave it
|
|
74
|
+
// alone here; /cap:learn review clears it after the user processes the board, and a fresh
|
|
75
|
+
// shouldShowBoard==false simply means there's nothing new to add.
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Threshold met — write the flag. Eligible count is recomputed for diagnostic purposes;
|
|
80
|
+
// the SKILL checks for FILE EXISTENCE, not content, so a half-written flag is harmless.
|
|
81
|
+
let eligibleCount = 0;
|
|
82
|
+
try {
|
|
83
|
+
const board = review.buildReviewBoard(cwd);
|
|
84
|
+
if (board && Array.isArray(board.eligible)) eligibleCount = board.eligible.length;
|
|
85
|
+
} catch (_e) {
|
|
86
|
+
eligibleCount = 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// @cap-risk(F-073/AC-3) writeBoardPendingFlag sanitises sessionId before persistence so a
|
|
90
|
+
// hostile SESSION.json can't smuggle bytes via the flag content.
|
|
91
|
+
review.writeBoardPendingFlag(cwd, { eligibleCount });
|
|
92
|
+
process.exit(0);
|
|
93
|
+
} catch (_err) {
|
|
94
|
+
// Best-effort error log to .cap/learning/.errors.log so we can diagnose without leaking
|
|
95
|
+
// through the tool surface. Mirrors cap-learning-hook.js's error-log strategy.
|
|
96
|
+
try {
|
|
97
|
+
const cwd = process.cwd();
|
|
98
|
+
const errDir = path.join(cwd, '.cap', 'learning');
|
|
99
|
+
if (!fs.existsSync(errDir)) fs.mkdirSync(errDir, { recursive: true });
|
|
100
|
+
fs.appendFileSync(
|
|
101
|
+
path.join(errDir, '.errors.log'),
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
ts: new Date().toISOString(),
|
|
104
|
+
hook: 'cap-learn-review-hook',
|
|
105
|
+
message: _err && _err.message ? _err.message : String(_err),
|
|
106
|
+
}) + '\n',
|
|
107
|
+
'utf8',
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
// Even logging failed — stay silent.
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @cap-feature(feature:F-070) Learning-Signals Hook — PostToolUse entry point for AC-1 (editAfterWrite)
|
|
3
|
+
// and AC-2 (memory-ref). cap-hook-version: {{CAP_VERSION}}
|
|
4
|
+
//
|
|
5
|
+
// PostToolUse hook: fires after Edit / Write / MultiEdit / NotebookEdit / Read and emits learning
|
|
6
|
+
// signals into .cap/learning/signals/<type>.jsonl via the cap-learning-signals.cjs collector.
|
|
7
|
+
//
|
|
8
|
+
// Two responsibilities:
|
|
9
|
+
// 1. Cross-event editAfterWrite detection via a per-session persistent ledger
|
|
10
|
+
// (.cap/learning/signals/../state/written-files.jsonl). Hooks fire as fresh subprocesses, so an
|
|
11
|
+
// in-memory Set cannot bridge a Write event and a later Edit event — the ledger is the bridge.
|
|
12
|
+
// Write / MultiEdit / NotebookEdit append to the ledger; Edit checks the ledger and emits
|
|
13
|
+
// recordOverride({subType:'editAfterWrite'}) when there is a match for the same sessionId.
|
|
14
|
+
// 2. When a Read targets any path under .cap/memory/**/*.md (recursive), emit recordMemoryRef.
|
|
15
|
+
//
|
|
16
|
+
// AC-5 budget: <50ms per hook. The collector is sync JSONL append. The ledger read happens here, but
|
|
17
|
+
// the ledger is per-session (typical <100 lines) and we never read the signal JSONLs.
|
|
18
|
+
//
|
|
19
|
+
// Skip via CAP_SKIP_LEARNING_HOOK=1.
|
|
20
|
+
// Never exits non-zero: a failure here must not block the edit/read tool.
|
|
21
|
+
//
|
|
22
|
+
// Reject-Approval (AC-1 second flavour) is left as an integration gap — Claude Code's PreToolUse
|
|
23
|
+
// rejection signal is not observable from the matchers we have access to in this repo. See the
|
|
24
|
+
// @cap-decision below and the @cap-todo on AC-1.
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const os = require('node:os');
|
|
31
|
+
|
|
32
|
+
// @cap-todo(ac:F-070/AC-5) Skip switch for benchmarking and tests that don't want hook side effects.
|
|
33
|
+
if (process.env.CAP_SKIP_LEARNING_HOOK === '1') {
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Edit handled separately from Write/MultiEdit/NotebookEdit: only the latter three "create new content"
|
|
38
|
+
// in the file from the agent's perspective. An Edit is the user's correction. Both groups append to
|
|
39
|
+
// the ledger so a chain Edit→Edit on a previously-written file still trips editAfterWrite.
|
|
40
|
+
const WRITE_TOOLS = new Set(['Write', 'MultiEdit', 'NotebookEdit']);
|
|
41
|
+
const EDIT_TOOL = 'Edit';
|
|
42
|
+
const OBSERVED_WRITE_TOOLS = new Set([EDIT_TOOL, ...WRITE_TOOLS]);
|
|
43
|
+
const OBSERVED_READ_TOOLS = new Set(['Read']);
|
|
44
|
+
|
|
45
|
+
let input = '';
|
|
46
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
47
|
+
process.stdin.setEncoding('utf8');
|
|
48
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
49
|
+
process.stdin.on('end', () => {
|
|
50
|
+
clearTimeout(stdinTimeout);
|
|
51
|
+
run(input);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function tryRequire(modulePath) {
|
|
55
|
+
try { return require(modulePath); } catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// @cap-decision(F-070/D8) Lib resolution mirrors cap-tag-observer.js exactly: env override → colocated
|
|
59
|
+
// in-tree → installed under ~/.claude. Keeping the resolution path identical means an
|
|
60
|
+
// ops change to one hook applies to the other for free.
|
|
61
|
+
function resolveCollectorModule() {
|
|
62
|
+
const candidates = [];
|
|
63
|
+
if (process.env.CAP_LEARNING_LIB) candidates.push(process.env.CAP_LEARNING_LIB);
|
|
64
|
+
candidates.push(path.join(__dirname, '..', 'cap', 'bin', 'lib', 'cap-learning-signals.cjs'));
|
|
65
|
+
candidates.push(path.join(os.homedir(), '.claude', 'cap', 'bin', 'lib', 'cap-learning-signals.cjs'));
|
|
66
|
+
for (const p of candidates) {
|
|
67
|
+
const mod = tryRequire(p);
|
|
68
|
+
if (mod) return mod;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// @cap-decision(F-070/D9) Session id resolution: read .cap/SESSION.json synchronously. The file is small
|
|
74
|
+
// (~few hundred bytes) so the read is O(1) and well inside AC-5's 50ms budget.
|
|
75
|
+
function readSessionContext(cwd) {
|
|
76
|
+
try {
|
|
77
|
+
const p = path.join(cwd, '.cap', 'SESSION.json');
|
|
78
|
+
if (!fs.existsSync(p)) return { sessionId: null, featureId: null };
|
|
79
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
80
|
+
return {
|
|
81
|
+
sessionId: typeof parsed.sessionId === 'string' ? parsed.sessionId : null,
|
|
82
|
+
featureId: typeof parsed.activeFeature === 'string' ? parsed.activeFeature : null,
|
|
83
|
+
};
|
|
84
|
+
} catch { return { sessionId: null, featureId: null }; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isUnderMemoryDir(absPath, cwd) {
|
|
88
|
+
// Match any file under <cwd>/.cap/memory/ regardless of subdirectory or extension.
|
|
89
|
+
// The collector hashes the path; we just need a routing decision here.
|
|
90
|
+
const memoryRoot = path.join(cwd, '.cap', 'memory');
|
|
91
|
+
// Use startsWith with a path separator suffix to avoid matching e.g. .cap/memory-foo.
|
|
92
|
+
return absPath === memoryRoot
|
|
93
|
+
|| absPath.startsWith(memoryRoot + path.sep)
|
|
94
|
+
|| absPath.startsWith(memoryRoot + '/');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function run(raw) {
|
|
98
|
+
// @cap-todo(ac:F-070/AC-7) Whole hook body wrapped in try/catch; failures never escape.
|
|
99
|
+
try {
|
|
100
|
+
const data = raw ? JSON.parse(raw) : {};
|
|
101
|
+
const toolName = data.tool_name;
|
|
102
|
+
const toolInput = data.tool_input || {};
|
|
103
|
+
const cwd = data.cwd || process.cwd();
|
|
104
|
+
const filePath = toolInput.file_path || toolInput.notebook_path;
|
|
105
|
+
if (!toolName || !filePath) process.exit(0);
|
|
106
|
+
|
|
107
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
108
|
+
|
|
109
|
+
const collector = resolveCollectorModule();
|
|
110
|
+
if (!collector) process.exit(0); // library not installed — silent no-op, mirrors cap-memory.js
|
|
111
|
+
|
|
112
|
+
// @cap-todo(ac:F-070/AC-1) editAfterWrite detection across subprocess boundaries: the per-session
|
|
113
|
+
// ledger bridges what a single hook process cannot. Edit checks the ledger;
|
|
114
|
+
// all four tools append to the ledger so a subsequent Edit on the same path
|
|
115
|
+
// (this session) emits the override.
|
|
116
|
+
if (OBSERVED_WRITE_TOOLS.has(toolName)) {
|
|
117
|
+
const ctx = readSessionContext(cwd);
|
|
118
|
+
if (toolName === EDIT_TOOL && ctx.sessionId
|
|
119
|
+
&& collector.wasWrittenInSession(cwd, ctx.sessionId, absPath)) {
|
|
120
|
+
// @cap-risk(F-070/AC-5) recordOverride is sync JSONL append, never reads signal JSONLs.
|
|
121
|
+
// The ledger read above IS in the hot path but stays bounded
|
|
122
|
+
// (per-session file, typical <100 entries). The performance suite
|
|
123
|
+
// brackets the full hook to confirm the 50ms budget holds.
|
|
124
|
+
collector.recordOverride({
|
|
125
|
+
projectRoot: cwd,
|
|
126
|
+
subType: 'editAfterWrite',
|
|
127
|
+
sessionId: ctx.sessionId,
|
|
128
|
+
featureId: ctx.featureId,
|
|
129
|
+
targetFile: absPath, // collector hashes; never persisted raw
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Append to the persistent ledger so future Edit events in this session can detect the chain.
|
|
133
|
+
// We append for ALL four tools (Edit included) so Edit→Edit on a previously-written file still
|
|
134
|
+
// produces an override on the second Edit.
|
|
135
|
+
if (ctx.sessionId) {
|
|
136
|
+
collector.recordWriteIntoLedger(cwd, ctx.sessionId, absPath);
|
|
137
|
+
}
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// @cap-todo(ac:F-070/AC-2) memory-ref detection: a Read on .cap/memory/*.md → recordMemoryRef.
|
|
142
|
+
if (OBSERVED_READ_TOOLS.has(toolName)) {
|
|
143
|
+
if (!isUnderMemoryDir(absPath, cwd)) process.exit(0);
|
|
144
|
+
const ctx = readSessionContext(cwd);
|
|
145
|
+
collector.recordMemoryRef({
|
|
146
|
+
projectRoot: cwd,
|
|
147
|
+
sessionId: ctx.sessionId,
|
|
148
|
+
featureId: ctx.featureId,
|
|
149
|
+
memoryFile: absPath, // collector hashes
|
|
150
|
+
});
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Unobserved tool — exit silently.
|
|
155
|
+
process.exit(0);
|
|
156
|
+
} catch (_err) {
|
|
157
|
+
// AC-7: never propagate. Best-effort error log to .cap/learning/signals/.errors.log so we can
|
|
158
|
+
// diagnose without leaking through the tool surface.
|
|
159
|
+
try {
|
|
160
|
+
const cwd = process.cwd();
|
|
161
|
+
const errDir = path.join(cwd, '.cap', 'learning', 'signals');
|
|
162
|
+
if (!fs.existsSync(errDir)) fs.mkdirSync(errDir, { recursive: true });
|
|
163
|
+
fs.appendFileSync(
|
|
164
|
+
path.join(errDir, '.errors.log'),
|
|
165
|
+
JSON.stringify({
|
|
166
|
+
ts: new Date().toISOString(),
|
|
167
|
+
message: _err && _err.message ? _err.message : String(_err),
|
|
168
|
+
}) + '\n',
|
|
169
|
+
'utf8',
|
|
170
|
+
);
|
|
171
|
+
} catch {
|
|
172
|
+
// Even logging failed — stay silent.
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// @cap-todo(ac:F-070/AC-1) Reject-Approval flavour of recordOverride is INTENTIONALLY UNWIRED here.
|
|
179
|
+
// @cap-decision(F-070/D10) Reject-Approval is left as a documented integration gap.
|
|
180
|
+
// Why: PreToolUse rejection events in this repo's hook surface are not observable as a distinct
|
|
181
|
+
// tool_name / payload — the existing hooks (cap-prompt-guard, cap-workflow-guard) intercept BEFORE a
|
|
182
|
+
// tool runs but they do not report a "user rejected" signal back into the post-tool stream. Wiring a
|
|
183
|
+
// speculative shape would invent an interface that downstream Claude Code hook contract changes might
|
|
184
|
+
// silently drift away from.
|
|
185
|
+
// What's still good: the COLLECTOR exposes recordOverride({subType:'rejectApproval'}) and the unit
|
|
186
|
+
// tests cover that shape. Whoever wires the rejection signal later (whether via a distinct hook
|
|
187
|
+
// matcher or a stdin payload field we haven't seen yet) just needs to call the collector — no schema
|
|
188
|
+
// work, no module refactor. This keeps the gap honest: tested code path, undefined call site.
|
|
189
|
+
// @cap-risk(F-070/AC-1) The editAfterWrite half of AC-1 is fully wired across subprocess boundaries
|
|
190
|
+
// via the per-session ledger (cap-learning-signals#recordWriteIntoLedger / wasWrittenInSession) and
|
|
191
|
+
// covered by an end-to-end spawnSync test that drives Write→Edit through this hook. The rejectApproval
|
|
192
|
+
// half is collector-tested but has NO hook call site (D10).
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @cap-feature(feature:F-030) Memory Automation Hook — post-session hook that triggers memory accumulation pipeline
|
|
3
|
+
// @cap-history(sessions:2, edits:5, since:2026-05-06, learned:2026-05-08) Frequently modified — 2 sessions, 5 edits
|
|
4
|
+
// @cap-history(sessions:3, edits:9, since:2026-04-03, learned:2026-04-04) Frequently modified — 3 sessions, 9 edits
|
|
5
|
+
// cap-hook-version: {{CAP_VERSION}}
|
|
6
|
+
// Memory Hook - runs after session end to accumulate project memory.
|
|
7
|
+
//
|
|
8
|
+
// Pipeline: F-027 (Engine) → F-028 (Annotation Writer) → F-029 (Memory Directory)
|
|
9
|
+
//
|
|
10
|
+
// Two modes:
|
|
11
|
+
// Incremental (default): Only processes sessions newer than .cap/memory/.last-run
|
|
12
|
+
// Init (via /cap:memory init): Processes ALL sessions, builds initial memory
|
|
13
|
+
//
|
|
14
|
+
// Skip with CAP_SKIP_MEMORY=1 environment variable.
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
// @cap-todo(ref:F-030:AC-8) Hook skippable via CAP_SKIP_MEMORY=1
|
|
21
|
+
if (process.env.CAP_SKIP_MEMORY === '1') {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Resolve installed module paths
|
|
26
|
+
const homeDir = os.homedir();
|
|
27
|
+
const capLib = path.join(homeDir, '.claude', 'cap', 'bin', 'lib');
|
|
28
|
+
|
|
29
|
+
function tryRequire(modulePath) {
|
|
30
|
+
try { return require(modulePath); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LAST_RUN_FILE = '.last-run';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the last-run timestamp from .cap/memory/.last-run
|
|
37
|
+
* @param {string} cwd
|
|
38
|
+
* @returns {string|null} ISO timestamp or null if never run
|
|
39
|
+
*/
|
|
40
|
+
function readLastRun(cwd) {
|
|
41
|
+
const fp = path.join(cwd, '.cap', 'memory', LAST_RUN_FILE);
|
|
42
|
+
try {
|
|
43
|
+
return fs.readFileSync(fp, 'utf8').trim() || null;
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Write the current timestamp to .cap/memory/.last-run
|
|
49
|
+
* @param {string} cwd
|
|
50
|
+
*/
|
|
51
|
+
function writeLastRun(cwd) {
|
|
52
|
+
const dir = path.join(cwd, '.cap', 'memory');
|
|
53
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
fs.writeFileSync(path.join(dir, LAST_RUN_FILE), new Date().toISOString(), 'utf8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Filter session files to only those newer than a timestamp.
|
|
59
|
+
* @param {Array<{path: string, date: string|null}>} files
|
|
60
|
+
* @param {string|null} since - ISO timestamp
|
|
61
|
+
* @returns {Array}
|
|
62
|
+
*/
|
|
63
|
+
function filterNewSessions(files, since) {
|
|
64
|
+
if (!since) return files; // No last-run = process all
|
|
65
|
+
return files.filter(f => f.date && f.date > since);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// @cap-todo(ref:F-030:AC-1) Post-session hook triggers F-027→F-028→F-029 pipeline
|
|
69
|
+
// @cap-todo(ref:F-030:AC-7) Hook completes within 5 seconds for up to 50 session files
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run the memory pipeline.
|
|
73
|
+
* @param {Object} [options]
|
|
74
|
+
* @param {boolean} [options.init] - If true, process ALL sessions (bootstrap mode)
|
|
75
|
+
*/
|
|
76
|
+
function run(options = {}) {
|
|
77
|
+
const startTime = Date.now();
|
|
78
|
+
const cwd = process.cwd();
|
|
79
|
+
|
|
80
|
+
// Load modules
|
|
81
|
+
const extract = tryRequire(path.join(capLib, 'cap-session-extract.cjs'));
|
|
82
|
+
const engine = tryRequire(path.join(capLib, 'cap-memory-engine.cjs'));
|
|
83
|
+
const writer = tryRequire(path.join(capLib, 'cap-annotation-writer.cjs'));
|
|
84
|
+
const memDir = tryRequire(path.join(capLib, 'cap-memory-dir.cjs'));
|
|
85
|
+
|
|
86
|
+
if (!extract || !engine || !writer || !memDir) {
|
|
87
|
+
// Modules not installed — skip silently
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Find project sessions — monorepo-aware for init, single-project for incremental
|
|
92
|
+
let allSessionFiles;
|
|
93
|
+
let projectInfo;
|
|
94
|
+
|
|
95
|
+
if (options.init && extract.getAllSessionFiles) {
|
|
96
|
+
// Init mode: scan all sub-project sessions (monorepo-aware)
|
|
97
|
+
const result = extract.getAllSessionFiles(cwd);
|
|
98
|
+
allSessionFiles = result.files;
|
|
99
|
+
projectInfo = result.projects;
|
|
100
|
+
} else {
|
|
101
|
+
// Incremental mode: single project only (fast)
|
|
102
|
+
const projectDir = extract.getProjectDir(cwd);
|
|
103
|
+
if (!projectDir) return;
|
|
104
|
+
allSessionFiles = extract.getSessionFiles(projectDir);
|
|
105
|
+
projectInfo = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (allSessionFiles.length === 0) return;
|
|
109
|
+
|
|
110
|
+
// Incremental: only process sessions since last run (unless init mode)
|
|
111
|
+
const lastRun = options.init ? null : readLastRun(cwd);
|
|
112
|
+
const sessionFiles = filterNewSessions(allSessionFiles, lastRun);
|
|
113
|
+
|
|
114
|
+
if (sessionFiles.length === 0) return; // Nothing new
|
|
115
|
+
|
|
116
|
+
// Detect debug sessions from SESSION.json
|
|
117
|
+
let activeDebug = false;
|
|
118
|
+
try {
|
|
119
|
+
const sessionPath = path.join(cwd, '.cap', 'SESSION.json');
|
|
120
|
+
if (fs.existsSync(sessionPath)) {
|
|
121
|
+
const session = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
|
122
|
+
activeDebug = session.step === 'debug' || session.activeDebugSession != null;
|
|
123
|
+
}
|
|
124
|
+
} catch { /* ignore */ }
|
|
125
|
+
|
|
126
|
+
// --- Primary source: Code tags (single source of truth) ---
|
|
127
|
+
const scanner = tryRequire(path.join(capLib, 'cap-tag-scanner.cjs'));
|
|
128
|
+
let codeEntries = [];
|
|
129
|
+
if (scanner) {
|
|
130
|
+
const tags = scanner.scanDirectory(cwd, { projectRoot: cwd });
|
|
131
|
+
codeEntries = engine.accumulateFromCode(tags);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Secondary source: Sessions (hotspots only — edit frequency) ---
|
|
135
|
+
// @cap-decision Hotspots are inherently cumulative (a "hotspot" requires >= minHotspot sessions
|
|
136
|
+
// of edits to a single file). In incremental mode the new-session window is too
|
|
137
|
+
// narrow — usually 1–2 sessions — so naive filtering produces zero hotspots, and
|
|
138
|
+
// writeMemoryDirectory then overwrites hotspots.md with an empty stub. Real-world
|
|
139
|
+
// evidence: GoetzeInvest had 61 hotspot nodes in graph.json from a prior init run,
|
|
140
|
+
// but hotspots.md was a 202-byte "_No hotspots recorded yet._" header after every
|
|
141
|
+
// incremental run. Compute hotspots from ALL sessions every run; the
|
|
142
|
+
// filterNewSessions short-circuit above (early-return when sessionFiles is empty)
|
|
143
|
+
// still gates whether we run at all, so we don't burn IO on no-op invocations.
|
|
144
|
+
const filesToProcess = allSessionFiles.map(f => ({
|
|
145
|
+
path: f.path,
|
|
146
|
+
isDebugSession: activeDebug,
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
const sessionResult = engine.accumulateFromFiles(filesToProcess, { projectRoot: cwd });
|
|
150
|
+
|
|
151
|
+
// Merge: code-based decisions/pitfalls + session-based hotspots (full-history)
|
|
152
|
+
const allEntries = [...codeEntries, ...sessionResult.newEntries];
|
|
153
|
+
|
|
154
|
+
if (allEntries.length === 0 && sessionResult.staleEntries.length === 0) {
|
|
155
|
+
writeLastRun(cwd);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// F-028: Write hotspot annotations into source files (only annotatable source code)
|
|
160
|
+
// Code-based decisions/pitfalls are ALREADY in the code as @cap-decision/@cap-todo — no need to re-annotate.
|
|
161
|
+
const NON_ANNOTATABLE_EXT = new Set(['.md', '.markdown', '.json', '.jsonl', '.lock', '.svg', '.xml', '.html', '.css', '.scss']);
|
|
162
|
+
const fileEntries = {};
|
|
163
|
+
for (const entry of allEntries) {
|
|
164
|
+
if (entry.category !== 'hotspot') continue; // Only write hotspot annotations
|
|
165
|
+
if (entry.file && fs.existsSync(entry.file)) {
|
|
166
|
+
const ext = path.extname(entry.file).toLowerCase();
|
|
167
|
+
if (NON_ANNOTATABLE_EXT.has(ext)) continue;
|
|
168
|
+
if (!fileEntries[entry.file]) fileEntries[entry.file] = [];
|
|
169
|
+
fileEntries[entry.file].push(entry);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (Object.keys(fileEntries).length > 0) {
|
|
174
|
+
writer.writeAnnotations(fileEntries);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// F-028: Remove stale annotations
|
|
178
|
+
if (sessionResult.staleEntries.length > 0) {
|
|
179
|
+
writer.removeStaleAnnotations(sessionResult.staleEntries);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// F-029: Write memory directory (merge mode for multi-developer support)
|
|
183
|
+
// @cap-feature(feature:F-090, primary:true) Apply confidence-filter at the hook layer:
|
|
184
|
+
// only entries with confidence >= 0.6 OR pinned land in the .md output. graph.json is
|
|
185
|
+
// built independently and stays full (Cluster/Affinity components need every node).
|
|
186
|
+
// Real-world driver: GoetzeInvest hub had 568 KB / 2340 entries in decisions.md, ~95%
|
|
187
|
+
// Confidence:0.50/Evidence:1 heuristic noise. Filter brings agent session-start cost
|
|
188
|
+
// from ~150k tokens to ~5–15k tokens for typical projects.
|
|
189
|
+
// @cap-decision(F-090) Threshold lives in the hook (not in writeMemoryDirectory) so direct
|
|
190
|
+
// callers (tests, CLI tools, library consumers) keep backwards-compat behavior.
|
|
191
|
+
memDir.writeMemoryDirectory(cwd, allEntries, { merge: !options.init, minConfidence: 0.6 });
|
|
192
|
+
|
|
193
|
+
// @cap-decision(F-079/iter1) Stage-2 #1 fix: processSnapshots wired into memory-pipeline.
|
|
194
|
+
// Closes AC-4 — "Memory-Pipeline MUSS Snapshots ... referenzieren" — by invoking
|
|
195
|
+
// processSnapshots() after writeMemoryDirectory so per-feature/platform files get a
|
|
196
|
+
// populated linked_snapshots block on every pipeline run. Idempotent: byte-identical on
|
|
197
|
+
// re-run because processSnapshots groups by target and writes ONE upsert per target with
|
|
198
|
+
// the FULL set. Wrapped in try/catch so a snapshot-linkage failure never blocks memory.
|
|
199
|
+
// @cap-decision(F-079/followup) F-079-FIX-C: hook surfaces processSnapshots skip-count.
|
|
200
|
+
// Previously the hook ignored the `skipped` field from the result. If a snapshot fails
|
|
201
|
+
// to be linked (parse-error, malformed frontmatter), it silently disappeared from the
|
|
202
|
+
// diagnostic surface. Now we summarize the skipped names + reasons via the project's
|
|
203
|
+
// debug logger (CAP_DEBUG=1 surfaces it; default path stays silent to honor the
|
|
204
|
+
// "best-effort, never block" contract). Surfaced as a single line for grep-friendliness.
|
|
205
|
+
const linkage = tryRequire(path.join(capLib, 'cap-snapshot-linkage.cjs'));
|
|
206
|
+
if (linkage && linkage.processSnapshots) {
|
|
207
|
+
try {
|
|
208
|
+
const result = linkage.processSnapshots(cwd, {});
|
|
209
|
+
if (result && Array.isArray(result.skipped) && result.skipped.length > 0) {
|
|
210
|
+
const logger = tryRequire(path.join(capLib, 'cap-logger.cjs'));
|
|
211
|
+
if (logger && typeof logger.debug === 'function') {
|
|
212
|
+
const summary = result.skipped.map((s) => {
|
|
213
|
+
const name = (s && typeof s.name === 'string') ? s.name : '<unknown>';
|
|
214
|
+
const reason = (s && typeof s.reason === 'string') ? s.reason : '<no-reason>';
|
|
215
|
+
return `${name}=${reason}`;
|
|
216
|
+
}).join(',');
|
|
217
|
+
logger.debug({
|
|
218
|
+
op: 'cap-memory-hook.processSnapshots',
|
|
219
|
+
errorType: 'snapshot-skipped',
|
|
220
|
+
errorMessage: `processSnapshots skipped ${result.skipped.length} snapshot(s): ${summary}`,
|
|
221
|
+
recoveryAction: 'continued pipeline; per-feature/platform writes succeeded for non-skipped snapshots',
|
|
222
|
+
});
|
|
223
|
+
} else if (process.env.CAP_DEBUG) {
|
|
224
|
+
// Fallback when logger isn't available (defense-in-depth — CAP_DEBUG users still see something).
|
|
225
|
+
try {
|
|
226
|
+
process.stderr.write(`[cap:debug] processSnapshots skipped ${result.skipped.length} snapshot(s)\n`);
|
|
227
|
+
} catch (_eDbg) { /* ignore */ }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (_e) {
|
|
231
|
+
// Snapshot linkage is best-effort; never block the rest of the pipeline.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// F-034: Update memory graph
|
|
236
|
+
const memGraph = tryRequire(path.join(capLib, 'cap-memory-graph.cjs'));
|
|
237
|
+
if (memGraph) {
|
|
238
|
+
try {
|
|
239
|
+
if (options.init) {
|
|
240
|
+
// Full rebuild from all sources
|
|
241
|
+
const graph = memGraph.buildFromMemory(cwd);
|
|
242
|
+
memGraph.saveGraph(cwd, graph);
|
|
243
|
+
} else {
|
|
244
|
+
// Incremental update with new entries
|
|
245
|
+
const graph = memGraph.loadGraph(cwd);
|
|
246
|
+
const staleNodeIds = sessionResult.staleEntries.map(
|
|
247
|
+
e => memGraph.generateNodeId(e.category, e.content)
|
|
248
|
+
);
|
|
249
|
+
memGraph.incrementalUpdate(graph, allEntries, { staleNodeIds });
|
|
250
|
+
memGraph.saveGraph(cwd, graph);
|
|
251
|
+
}
|
|
252
|
+
} catch (_e) {
|
|
253
|
+
// Graph update is non-critical — don't block session end
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Save last-run timestamp
|
|
258
|
+
writeLastRun(cwd);
|
|
259
|
+
|
|
260
|
+
// Stats for reporting
|
|
261
|
+
const stats = {
|
|
262
|
+
decisions: allEntries.filter(e => e.category === 'decision').length,
|
|
263
|
+
pitfalls: allEntries.filter(e => e.category === 'pitfall').length,
|
|
264
|
+
patterns: allEntries.filter(e => e.category === 'pattern').length,
|
|
265
|
+
hotspots: allEntries.filter(e => e.category === 'hotspot').length,
|
|
266
|
+
fromCode: codeEntries.length,
|
|
267
|
+
fromSessions: sessionResult.newEntries.length,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Performance check
|
|
271
|
+
const elapsed = Date.now() - startTime;
|
|
272
|
+
if (elapsed > 5000 && !options.init) {
|
|
273
|
+
process.stderr.write(`cap-memory: warning — hook took ${elapsed}ms (target: <5000ms)\n`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Report in init mode
|
|
277
|
+
if (options.init) {
|
|
278
|
+
const elapsed2 = Date.now() - startTime;
|
|
279
|
+
if (projectInfo && projectInfo.length > 1) {
|
|
280
|
+
process.stdout.write(`cap-memory init: monorepo mode — ${projectInfo.length} sub-projects found\n`);
|
|
281
|
+
for (const p of projectInfo) {
|
|
282
|
+
process.stdout.write(` ${p}\n`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
process.stdout.write(`cap-memory init: ${sessionFiles.length} sessions, ${stats.fromCode} code tags processed in ${elapsed2}ms\n`);
|
|
286
|
+
process.stdout.write(` decisions: ${stats.decisions} (from code), pitfalls: ${stats.pitfalls} (from code), hotspots: ${stats.hotspots} (from sessions)\n`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// CLI mode: support "init" argument for bootstrap
|
|
291
|
+
const args = process.argv.slice(2);
|
|
292
|
+
const isInit = args.includes('init') || args.includes('--init');
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
run({ init: isInit });
|
|
296
|
+
} catch (err) {
|
|
297
|
+
// Never block session end — fail silently
|
|
298
|
+
process.stderr.write(`cap-memory: ${err.message}\n`);
|
|
299
|
+
}
|