forgetrail 0.4.14 → 0.4.15

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.
Files changed (43) hide show
  1. package/INITIAL_PROMPT.md +1 -1
  2. package/README.md +1 -1
  3. package/TRY_FORGETRAIL.md +44 -5
  4. package/WORKFLOW.md +15 -2
  5. package/content/AGENT_INTEGRATION_claude.md +3 -2
  6. package/content/AGENT_INTEGRATION_cursor.md +2 -1
  7. package/content/COMPANION_TOOLS.md +1 -0
  8. package/content/FORGETRAIL_LITE.md +41 -75
  9. package/content/GREENFIELD_INTAKE.md +9 -2
  10. package/content/LITE_WORKFLOW_TRACKING.json +2 -1
  11. package/content/NEW_PROJECT_BOOTSTRAP.md +2 -3
  12. package/content/companion-tools.json +18 -0
  13. package/content/cursor-rules/npm-name-hold.mdc +1 -1
  14. package/content/cursor-rules/spec-completion.mdc +4 -1
  15. package/content/cursor-rules/specs-and-todo.mdc +5 -1
  16. package/content/cursor-rules/url-host-matching.mdc +12 -13
  17. package/content/cursor-rules/user-facing-content.mdc +2 -2
  18. package/content/cursor-rules/writing-voice.mdc +6 -0
  19. package/content/forgetrail-workspace-README.md +2 -4
  20. package/content/hooks/README.md +36 -0
  21. package/content/hooks/claude-settings-hooks.json +20 -0
  22. package/content/hooks/cursor-hooks.json +30 -0
  23. package/content/hooks/guard-edit.mjs +85 -0
  24. package/content/hooks/guard-shell.mjs +168 -0
  25. package/content/hooks/session-start.mjs +93 -0
  26. package/content/hooks/session-stop.mjs +56 -0
  27. package/content/hooks/validate-tracking-core.mjs +229 -0
  28. package/content/hooks/validate-tracking.mjs +65 -0
  29. package/docs/SPEC_FEATURE_TEMPLATE.md +12 -4
  30. package/docs/TECHNICAL_REFERENCE.md +1 -1
  31. package/package.json +2 -1
  32. package/prompts/microcopy-centralization.md +1 -1
  33. package/prompts/pre-launch-audit.md +8 -0
  34. package/prompts/propagate-to-forgetrail.md +1 -1
  35. package/prompts/rules-audit.md +74 -0
  36. package/prompts/second-family-critique.md +43 -0
  37. package/prompts/spec-review.md +65 -0
  38. package/scripts/forgetrail-cli.mjs +6 -0
  39. package/scripts/install.mjs +43 -1
  40. package/scripts/workspace-index.mjs +128 -0
  41. package/workflow_tracking.json +1 -0
  42. package/content/cursor-rules/forgetrail-no-trailer.mdc +0 -49
  43. package/content/cursor-rules/us-english.mdc +0 -9
@@ -1,31 +1,30 @@
1
1
  ---
2
- description: Prefer hostMatchesDomain over hostname.includes for board/ATS URL checks
2
+ description: Prefer exact domain/suffix matching over substring includes for URL checks and SSRF hygiene
3
3
  globs:
4
4
  - src/**/*.{ts,js}
5
- - outpost/**/*.{js,mjs}
6
5
  - scripts/**/*.{js,mjs,ts}
7
6
  alwaysApply: false
8
7
  ---
9
8
 
10
- # URL host matching
9
+ # URL host matching and SSRF hygiene
11
10
 
12
- ## Prefer shared helpers
11
+ ## Domain and host checks
13
12
 
14
- - For board / ATS / “is this LinkedIn / Indeed / Greenhouse?” decisions, use **`hostMatchesDomain` / `urlMatchesHostDomain`** from `$lib/urlHost` (Outpost: `outpost/src/urlHost.js`, keep in sync).
15
- - **Do not** write `hostname.includes('linkedin.com')` or `url.includes('indeed.com')` for host allow/deny. Substring checks match lookalikes (`evil-linkedin.com`, `linkedin.com.attacker.example`) and trip CodeQL `js/incomplete-url-substring-sanitization`.
13
+ - For external integration and host allow/deny decisions, use exact domain or suffix matching (`endsWith('.example.com')` or a shared helper like `hostMatchesDomain`).
14
+ - **Do not** write `hostname.includes('example.com')` or `url.includes('example.com')` for host allow/deny. Substring checks match attacker lookalikes (`evil-example.com`, `example.com.attacker.net`) and trip CodeQL `js/incomplete-url-substring-sanitization`.
16
15
 
17
16
  ## Parse first
18
17
 
19
- - Prefer `new URL(...)` then match on **`hostname`**. Keep path checks on **`pathname`** (e.g. `/jobs/view`), not on the full href string when tightening.
18
+ - Prefer `new URL(...)` then match on **`hostname`**. Keep path checks on **`pathname`** (e.g. `/api/v1`), not on the full href string.
20
19
 
21
- ## Outgoing fetch
20
+ ## Outgoing fetch (SSRF hygiene)
22
21
 
23
- - App: `assertSafeUrl` at the boundary; follow redirects with re-check (`safeFetch`).
24
- - Outpost: `assertPublicHttpUrl` before slot acquisition / `goto`. Align blocked hosts (localhost, private IPs, `.local` / `.internal`, credentialed URLs).
22
+ - Validate external URLs before slot acquisition or server-side fetch.
23
+ - Disallow access to local and private network ranges: `localhost`, `127.0.0.1`, `::1`, RFC 1918 private IPs (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), `.local`, `.internal`, and credentials in URLs (`user:pass@host`).
24
+ - Follow redirects with re-validation of each redirect target.
25
25
 
26
26
  ## Schemes and sanitization
27
27
 
28
28
  - Scheme checks: **allowlist** `http:` / `https:`; do not only deny `javascript:`.
29
- - Tag stripping / entity decode: use shared helpers (`stripMarkupTags`, amp-last entity decode) or a parser. If a regex is extraction-only, say so in a one-line comment.
30
- - GitHub Actions: always set top-level `permissions:` (least privilege).
31
- - Never return `err.stack` or raw exception text to HTTP clients; log server-side and return stable error codes.
29
+ - Tag stripping / entity decode: use shared parser utilities. If a regex is extraction-only, annotate it in a comment.
30
+ - Never return `err.stack` or raw exception text to external HTTP clients; log server-side and return stable error codes.
@@ -19,8 +19,8 @@ alwaysApply: true
19
19
 
20
20
  When describing what **the product does**, use direct verbs — not hedging **can**:
21
21
 
22
- - **Do:** “Resume Forge **strengthens** your file.” / “The Prospect Engine **suggests** matching roles.” / “Foreman **proposes** a run plan.”
23
- - **Don't:** “…**can strengthen**…” / “…**can suggest**…” when stating a shipped capability.
22
+ - **Do:** “The pipeline **validates** each record.” / “The analyzer **identifies** bottlenecks.” / “The engine **generates** clean exports.”
23
+ - **Don't:** “…**can validate**…” / “…**can identify**…” when stating a shipped capability.
24
24
 
25
25
  **Keep *can* when it means:**
26
26
 
@@ -28,6 +28,12 @@ Fill the bracketed placeholders once brand voice crystallizes (usually after sev
28
28
  - **Ban vague nouns** in marketing and narrative copy: "something," "stuff," "things" (fine in error strings). Name the thing.
29
29
  - **Ban squish intensifiers:** "strongly," "deeply," "easily," "incredibly," "very." If the sentence needs the intensifier, rewrite the sentence.
30
30
 
31
+ ## Language and locale
32
+
33
+ - Default to **US English** conventions unless context indicates otherwise.
34
+ - **Adapt to the audience:** Adapt spelling and phrasing to what is discovered about the user, team, or target audience (e.g. Commonwealth/UK English when operating in those markets).
35
+ - **No em dashes (—) in user-facing copy:** Use a colon, comma, period, or parentheses instead.
36
+
31
37
  ## Named-person constraints (narratives)
32
38
 
33
39
  If real people appear in origin/brand narratives, record their hard constraints here (employer safety, public roles, verb restrictions like "no founder verbs for X") and audit after every pass. Keep narrative locks in a constraints file next to the long-form (LOCKED FRAMING / ARC / VOICE pattern).
@@ -12,9 +12,9 @@ Used for **both** ForgeTrail Lite file bootstrap and **MCP greenfield** kickoff.
12
12
  | `FORGETRAIL_LITE.md` | Full Lite kickoff protocol (Lite file bootstrap only) |
13
13
  | `FORGETRAIL_LITE_UPDATES.md` | Optional local feedback log (§1.6) — merge accepted items upstream |
14
14
  | `AGENTS.md` | Agent instructions — cite or symlink for your IDE |
15
- | `CLAUDE.md` | Claude Code trailer override |
15
+ | `CLAUDE.md` | Claude Code instructions |
16
16
  | `IDEAS.md` | Backlog parking lot |
17
- | `cursor/rules/*.mdc` | Cursor rule snippets (no-trailer, updates-log, …) |
17
+ | `cursor/rules/*.mdc` | Cursor rule snippets (updates-log, …) |
18
18
 
19
19
  ## Wire up Cursor (one-time)
20
20
 
@@ -24,7 +24,6 @@ From the **repo root**:
24
24
 
25
25
  ```bash
26
26
  mkdir -p .cursor/rules
27
- ln -sf ../../.forgetrail/cursor/rules/forgetrail-no-trailer.mdc .cursor/rules/
28
27
  ln -sf ../../.forgetrail/cursor/rules/forgetrail-updates-log.mdc .cursor/rules/
29
28
  ```
30
29
 
@@ -32,7 +31,6 @@ ln -sf ../../.forgetrail/cursor/rules/forgetrail-updates-log.mdc .cursor/rules/
32
31
 
33
32
  ```bat
34
33
  mkdir .cursor\rules 2>nul
35
- mklink .cursor\rules\forgetrail-no-trailer.mdc ..\.forgetrail\cursor\rules\forgetrail-no-trailer.mdc
36
34
  mklink .cursor\rules\forgetrail-updates-log.mdc ..\.forgetrail\cursor\rules\forgetrail-updates-log.mdc
37
35
  ```
38
36
 
@@ -0,0 +1,36 @@
1
+ # ForgeTrail Hooks
2
+
3
+ Enforces ForgeTrail rules and guards at the host tool level, rather than relying only on prompt requests.
4
+
5
+ ## What is in this directory
6
+
7
+ - `guard-shell.mjs`: runs pre-commit verification (`pnpm run verify`), prevents npm/yarn when `pnpm-lock.yaml` is present, gates `git push` to upstream remotes (pushing to IngotVault or local backup mirrors is allowed automatically), and prompts on destructive git or filesystem operations.
8
+ - `guard-edit.mjs`: guards `.env*` secrets files and `specs/completed/**` or `specs/canonical/**` records against accidental mutation.
9
+ - `session-start.mjs`: on session start, reads `.forgetrail/workflow_tracking.json` and injects live phase status, open exit criteria, and last session notes into agent context.
10
+ - `validate-tracking.mjs`: after editing `workflow_tracking.json`, runs structural validation and returns any issues as context.
11
+ - `validate-tracking-core.mjs`: standalone zero-dependency tracking validator.
12
+ - `session-stop.mjs`: on session stop, checks that session notes were added to tracking.
13
+ - `cursor-hooks.json`: standard Cursor hooks configuration.
14
+ - `claude-settings-hooks.json`: standard Claude Code configuration fragment.
15
+
16
+ ## Host setup
17
+
18
+ ### Cursor
19
+
20
+ Copy or link `cursor-hooks.json` to `.cursor/hooks.json` in your repository root. The hook scripts reside in `.forgetrail/hooks/`.
21
+
22
+ ### Claude Code
23
+
24
+ Add the contents of `claude-settings-hooks.json` to `.claude/settings.json` in your project root or user configuration.
25
+
26
+ ## Verification
27
+
28
+ Run any script with test input via stdin:
29
+
30
+ ```bash
31
+ echo '{"command": "npm install lodash"}' | node .forgetrail/hooks/guard-shell.mjs
32
+ # Output: {"permission":"deny", ...} (when pnpm-lock.yaml is present)
33
+
34
+ echo '{"command": "git push"}' | node .forgetrail/hooks/guard-shell.mjs
35
+ # Output: {"permission":"ask", ...}
36
+ ```
@@ -0,0 +1,20 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash|shell",
6
+ "command": "node .forgetrail/hooks/guard-shell.mjs"
7
+ },
8
+ {
9
+ "matcher": "Write|Edit|StrReplace",
10
+ "command": "node .forgetrail/hooks/guard-edit.mjs"
11
+ }
12
+ ],
13
+ "PostToolUse": [
14
+ {
15
+ "matcher": "Write|Edit|StrReplace",
16
+ "command": "node .forgetrail/hooks/validate-tracking.mjs"
17
+ }
18
+ ]
19
+ }
20
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "beforeShellExecution": [
5
+ {
6
+ "command": "node .forgetrail/hooks/guard-shell.mjs"
7
+ }
8
+ ],
9
+ "preToolUse": [
10
+ {
11
+ "command": "node .forgetrail/hooks/guard-edit.mjs"
12
+ }
13
+ ],
14
+ "sessionStart": [
15
+ {
16
+ "command": "node .forgetrail/hooks/session-start.mjs"
17
+ }
18
+ ],
19
+ "afterFileEdit": [
20
+ {
21
+ "command": "node .forgetrail/hooks/validate-tracking.mjs"
22
+ }
23
+ ],
24
+ "stop": [
25
+ {
26
+ "command": "node .forgetrail/hooks/session-stop.mjs"
27
+ }
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ForgeTrail Edit Guard
5
+ * Hook event: preToolUse (Cursor) / PreToolUse (Claude Code)
6
+ * Guards against accidental mutation of secrets (.env*) and completed/canonical specs.
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+
11
+ function readStdin() {
12
+ try {
13
+ const raw = readFileSync(0, "utf-8");
14
+ if (raw.trim()) return JSON.parse(raw);
15
+ } catch {}
16
+ return {};
17
+ }
18
+
19
+ function extractTargetPaths(tool, input) {
20
+ if (!input) return [];
21
+ const paths = [];
22
+ if (typeof input.path === "string") paths.push(input.path);
23
+ if (typeof input.file_path === "string") paths.push(input.file_path);
24
+ if (typeof input.target_file === "string") paths.push(input.target_file);
25
+ return paths;
26
+ }
27
+
28
+ function main() {
29
+ const payload = readStdin();
30
+ const tool = payload.tool || payload.tool_name || "";
31
+ const input = payload.input || payload.tool_input || {};
32
+
33
+ // Only check write/edit tools
34
+ const isEditTool =
35
+ /^(Write|Edit|StrReplace|WriteFile|file_editor|edit_file)$/i.test(tool);
36
+
37
+ if (!isEditTool) {
38
+ console.log(JSON.stringify({ permission: "allow" }));
39
+ return;
40
+ }
41
+
42
+ const paths = extractTargetPaths(tool, input);
43
+
44
+ for (const path of paths) {
45
+ const normalized = path.replace(/\\/g, "/");
46
+
47
+ // 1. Guard environment secrets (.env, .env.local, etc. but allow .env.example)
48
+ if (/(^|\/)\.env(\.[a-zA-Z0-9_-]+)?$/.test(normalized)) {
49
+ if (!normalized.endsWith(".env.example")) {
50
+ console.log(
51
+ JSON.stringify({
52
+ permission: "ask",
53
+ user_message:
54
+ "ForgeTrail Safety: Modifying environment secrets file (" +
55
+ path +
56
+ "). Please confirm before proceeding.",
57
+ agent_message:
58
+ "Editing environment secrets file requires user confirmation.",
59
+ })
60
+ );
61
+ return;
62
+ }
63
+ }
64
+
65
+ // 2. Guard completed and canonical delivery specs
66
+ if (/(^|\/)specs\/(completed|canonical)\//.test(normalized)) {
67
+ console.log(
68
+ JSON.stringify({
69
+ permission: "ask",
70
+ user_message:
71
+ "ForgeTrail Safety: Completed delivery specs (" +
72
+ path +
73
+ ") are historical records. Modify only if intentionally updating links or summaries.",
74
+ agent_message:
75
+ "Editing completed/canonical spec requires user confirmation.",
76
+ })
77
+ );
78
+ return;
79
+ }
80
+ }
81
+
82
+ console.log(JSON.stringify({ permission: "allow" }));
83
+ }
84
+
85
+ main();
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ForgeTrail Shell Guard
5
+ * Hook event: beforeShellExecution (Cursor) / PreToolUse (Claude Code)
6
+ * Enforces Lite §0, git commit attribution rules, pnpm consistency, and git push guards.
7
+ */
8
+
9
+ import { readFileSync, existsSync } from "node:fs";
10
+ import { resolve, join } from "node:path";
11
+ import { execSync } from "node:child_process";
12
+
13
+ function readStdin() {
14
+ try {
15
+ const raw = readFileSync(0, "utf-8");
16
+ if (raw.trim()) return JSON.parse(raw);
17
+ } catch {}
18
+ return {};
19
+ }
20
+
21
+ function hasPnpmLock(cwd) {
22
+ let curr = resolve(cwd || process.cwd());
23
+ for (let i = 0; i < 6; i++) {
24
+ if (existsSync(join(curr, "pnpm-lock.yaml"))) return true;
25
+ const parent = resolve(curr, "..");
26
+ if (parent === curr) break;
27
+ curr = parent;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ function hasVerifyScript(cwd) {
33
+ try {
34
+ const pkgPath = join(cwd || process.cwd(), "package.json");
35
+ if (existsSync(pkgPath)) {
36
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
37
+ return Boolean(pkg.scripts && pkg.scripts.verify);
38
+ }
39
+ } catch {}
40
+ return false;
41
+ }
42
+
43
+ function main() {
44
+ const input = readStdin();
45
+ const command = (input.command || input.tool_input?.command || "").trim();
46
+ const cwd = input.cwd || process.cwd();
47
+
48
+ if (!command) {
49
+ console.log(JSON.stringify({ permission: "allow" }));
50
+ return;
51
+ }
52
+
53
+ // 1. Verify-before-commit: run whenever package.json has a verify script (unless skipped via FORGETRAIL_SKIP_VERIFY=1 or --no-verify)
54
+ if (/\bgit\s+commit\b/.test(command)) {
55
+ if (
56
+ process.env.FORGETRAIL_SKIP_VERIFY !== "1" &&
57
+ !/\s+--no-verify\b/.test(command) &&
58
+ hasVerifyScript(cwd)
59
+ ) {
60
+ try {
61
+ execSync("pnpm run verify", { cwd, stdio: "pipe" });
62
+ } catch (err) {
63
+ console.log(
64
+ JSON.stringify({
65
+ permission: "deny",
66
+ user_message:
67
+ "ForgeTrail Verification: 'pnpm run verify' failed. Code checks must pass before committing.",
68
+ agent_message:
69
+ "Pre-commit verification failed. Run pnpm verify and fix errors before committing.",
70
+ })
71
+ );
72
+ return;
73
+ }
74
+ }
75
+ }
76
+
77
+ // 2. Check for git push (Do not push to upstream remote unless explicitly requested by user)
78
+ if (/\bgit\s+push\b/.test(command)) {
79
+ if (/\s+(-f|--force|--force-with-lease)\b/.test(command)) {
80
+ console.log(
81
+ JSON.stringify({
82
+ permission: "ask",
83
+ user_message:
84
+ "ForgeTrail Safety: Force-pushing to remote can overwrite git history. Please confirm if you wish to proceed.",
85
+ agent_message:
86
+ "Force-push requires explicit user confirmation.",
87
+ })
88
+ );
89
+ return;
90
+ }
91
+
92
+ // Pushing to IngotVault or local backup mirrors is automatic to preserve work
93
+ const isIngotVaultOrBackup =
94
+ /\bgit\s+push\b.*?\b(backup|ingotvault)\b/i.test(command) ||
95
+ /\bgit\s+push\b.*?(?:[a-zA-Z]:[\\/]|file:\/\/|\.\/|\.\.\/)/i.test(command);
96
+
97
+ if (isIngotVaultOrBackup) {
98
+ console.log(JSON.stringify({ permission: "allow" }));
99
+ return;
100
+ }
101
+
102
+ console.log(
103
+ JSON.stringify({
104
+ permission: "ask",
105
+ user_message:
106
+ "ForgeTrail Safety: git push to upstream remote requires explicit user approval per project rules. Would you like to push?",
107
+ agent_message:
108
+ "git push to upstream remote requires explicit user approval per project rules.",
109
+ })
110
+ );
111
+ return;
112
+ }
113
+
114
+ // 3. Check for npm/yarn when pnpm-lock exists (Lite §0 Rule 3)
115
+ if (/\b(npm\s+(i|install|add)|yarn(\s+add|\s+install)?)\b/.test(command)) {
116
+ if (hasPnpmLock(cwd)) {
117
+ console.log(
118
+ JSON.stringify({
119
+ permission: "deny",
120
+ user_message:
121
+ "ForgeTrail Lite §0: This repository uses pnpm (pnpm-lock.yaml detected). Do not run npm or yarn install.",
122
+ agent_message:
123
+ "Package manager is locked to pnpm. Run pnpm instead.",
124
+ })
125
+ );
126
+ return;
127
+ }
128
+ }
129
+
130
+ // 4. Check for destructive git operations
131
+ if (/\bgit\s+(reset\s+--hard|clean\s+-[a-zA-Z]*f)/.test(command)) {
132
+ console.log(
133
+ JSON.stringify({
134
+ permission: "ask",
135
+ user_message:
136
+ "ForgeTrail Safety: Destructive git operation (" +
137
+ command +
138
+ ") will permanently discard uncommitted changes. Confirm?",
139
+ agent_message: "Destructive git operation requires user approval.",
140
+ })
141
+ );
142
+ return;
143
+ }
144
+
145
+ // 5. Check for dangerous rm -rf outside build/temp
146
+ if (/\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+/.test(command)) {
147
+ const isSafe =
148
+ /\b(temp|build|\.svelte-kit|\.turbo|dist|\.wrangler|node_modules)[/\\]?/.test(
149
+ command
150
+ );
151
+ if (!isSafe) {
152
+ console.log(
153
+ JSON.stringify({
154
+ permission: "ask",
155
+ user_message:
156
+ "ForgeTrail Safety: Recursive file deletion outside temporary/build directories. Confirm?",
157
+ agent_message:
158
+ "Dangerous recursive deletion requires user approval.",
159
+ })
160
+ );
161
+ return;
162
+ }
163
+ }
164
+
165
+ console.log(JSON.stringify({ permission: "allow" }));
166
+ }
167
+
168
+ main();
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ForgeTrail Session Start Context Injector
5
+ * Hook event: sessionStart (Cursor)
6
+ * Injects live ForgeTrail phase status and open exit criteria into session context.
7
+ * Retires static phase-status rules on hosts with hook support.
8
+ */
9
+
10
+ import { readFileSync, existsSync } from "node:fs";
11
+ import { resolve, join } from "node:path";
12
+
13
+ function findTrackingFile(startDir) {
14
+ let curr = resolve(startDir || process.cwd());
15
+ for (let i = 0; i < 6; i++) {
16
+ const candidate1 = join(curr, ".forgetrail", "workflow_tracking.json");
17
+ if (existsSync(candidate1)) return candidate1;
18
+ const candidate2 = join(curr, "workflow_tracking.json");
19
+ if (existsSync(candidate2)) return candidate2;
20
+ const parent = resolve(curr, "..");
21
+ if (parent === curr) break;
22
+ curr = parent;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ function main() {
28
+ const trackingPath = findTrackingFile(process.cwd());
29
+
30
+ if (!trackingPath) {
31
+ console.log(JSON.stringify({}));
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const tracking = JSON.parse(readFileSync(trackingPath, "utf-8"));
37
+ const projectName = tracking.project?.name || "App";
38
+ const phase = tracking.currentPhase || "1";
39
+ const isLite = tracking.schemaVersion === "lite-1";
40
+
41
+ let remaining = [];
42
+ if (isLite) {
43
+ const phaseObj = tracking.phases?.[String(phase)];
44
+ if (phaseObj?.exitCriteria) {
45
+ remaining = Object.entries(phaseObj.exitCriteria)
46
+ .filter(([_, met]) => !met)
47
+ .map(([flag]) => flag);
48
+ }
49
+ } else {
50
+ const phaseObj = tracking.phases?.[phase];
51
+ if (Array.isArray(phaseObj?.exitCriteriaRemaining)) {
52
+ remaining = phaseObj.exitCriteriaRemaining;
53
+ }
54
+ }
55
+
56
+ let lastLeftOff = "";
57
+ if (Array.isArray(tracking.sessions) && tracking.sessions.length > 0) {
58
+ const lastSession = tracking.sessions[tracking.sessions.length - 1];
59
+ lastLeftOff = lastSession.leftOff || lastSession.notes || "";
60
+ }
61
+
62
+ const lines = [
63
+ `=== ForgeTrail Context (${projectName}) ===`,
64
+ `Current Phase: ${phase} (${isLite ? "Lite schema" : "MCP schema"})`,
65
+ ];
66
+
67
+ if (lastLeftOff) {
68
+ lines.push(`Last Session Left Off: ${lastLeftOff}`);
69
+ }
70
+
71
+ if (remaining.length > 0) {
72
+ lines.push("Open Exit Criteria for Current Phase:");
73
+ for (const item of remaining.slice(0, 5)) {
74
+ lines.push(` - ${item}`);
75
+ }
76
+ if (remaining.length > 5) {
77
+ lines.push(` ...and ${remaining.length - 5} more`);
78
+ }
79
+ }
80
+
81
+ lines.push("========================================");
82
+
83
+ console.log(
84
+ JSON.stringify({
85
+ additional_context: lines.join("\n"),
86
+ })
87
+ );
88
+ } catch {
89
+ console.log(JSON.stringify({}));
90
+ }
91
+ }
92
+
93
+ main();
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ForgeTrail Session Stop Check
5
+ * Hook event: stop (Cursor)
6
+ * Prompts a reminder if a session concluded without updating sessions[] in tracking.
7
+ */
8
+
9
+ import { readFileSync, existsSync } from "node:fs";
10
+ import { resolve, join } from "node:path";
11
+
12
+ function findTrackingFile(startDir) {
13
+ let curr = resolve(startDir || process.cwd());
14
+ for (let i = 0; i < 6; i++) {
15
+ const candidate1 = join(curr, ".forgetrail", "workflow_tracking.json");
16
+ if (existsSync(candidate1)) return candidate1;
17
+ const candidate2 = join(curr, "workflow_tracking.json");
18
+ if (existsSync(candidate2)) return candidate2;
19
+ const parent = resolve(curr, "..");
20
+ if (parent === curr) break;
21
+ curr = parent;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function main() {
27
+ const trackingPath = findTrackingFile(process.cwd());
28
+ if (!trackingPath) {
29
+ console.log(JSON.stringify({}));
30
+ return;
31
+ }
32
+
33
+ try {
34
+ const tracking = JSON.parse(readFileSync(trackingPath, "utf-8"));
35
+ const sessions = tracking.sessions;
36
+
37
+ const today = new Date().toISOString().slice(0, 10);
38
+ const hasTodaySession =
39
+ Array.isArray(sessions) &&
40
+ sessions.some((s) => s.date === today || (s.timestamp && s.timestamp.startsWith(today)));
41
+
42
+ if (!hasTodaySession) {
43
+ console.log(
44
+ JSON.stringify({
45
+ followup_message:
46
+ "ForgeTrail Reminder: Update .forgetrail/workflow_tracking.json (sessions[], decisions[], gotchas[]) with what was accomplished before ending.",
47
+ })
48
+ );
49
+ return;
50
+ }
51
+ } catch {}
52
+
53
+ console.log(JSON.stringify({}));
54
+ }
55
+
56
+ main();