forgetrail 0.4.20 → 0.5.1

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/CONTINUATION_PROMPT.md +5 -5
  2. package/INITIAL_PROMPT.md +15 -15
  3. package/README.md +5 -5
  4. package/TRACKING_SCHEMA.md +16 -7
  5. package/TRY_FORGETRAIL.md +3 -3
  6. package/WORKFLOW.md +50 -45
  7. package/content/AGENT_INTEGRATION_claude.md +1 -1
  8. package/content/AGENT_INTEGRATION_cursor.md +3 -3
  9. package/content/AGENT_INTEGRATION_generic.md +5 -5
  10. package/content/AGENT_INTEGRATION_grok.md +8 -8
  11. package/content/COMPANION_TOOLS.md +2 -2
  12. package/content/FORGETRAIL_LITE.md +72 -68
  13. package/content/FORGETRAIL_PROGRESS.md +3 -3
  14. package/content/GENESIS_SPEC_PROMPT.md +1 -1
  15. package/content/GENESIS_STUB.md +1 -1
  16. package/content/GREENFIELD_INTAKE.md +6 -6
  17. package/content/KICKOFF_WITHOUT_MCP.md +1 -1
  18. package/content/NEW_PROJECT_BOOTSTRAP.md +27 -27
  19. package/content/PLAN_MODE_PATTERNS.md +4 -4
  20. package/content/POST_BOOTSTRAP_USER_MESSAGE.md +1 -1
  21. package/content/SESSION_RESUME_MCP.md +7 -7
  22. package/content/companion-tools.json +5 -5
  23. package/content/cursor-rules/forgetrail-phase-status.mdc +8 -9
  24. package/content/examples/two-session-continuity.md +31 -67
  25. package/content/forgetrail-workspace-README.md +2 -2
  26. package/content/hooks/README.md +4 -4
  27. package/content/hooks/guard-shell.mjs +20 -12
  28. package/content/hooks/session-start.mjs +58 -63
  29. package/content/hooks/session-stop.mjs +49 -30
  30. package/content/hooks/validate-tracking-core.mjs +34 -1
  31. package/content/scripts/forgetrail-dev-launcher.mjs +12 -2
  32. package/content/skills/forgetrail/SKILL.md +21 -21
  33. package/content/skills/forgetrail/SKILL_FACTS.md +2 -2
  34. package/docs/CONTEXT_PROMPT.md +1 -1
  35. package/docs/NPM.md +8 -8
  36. package/docs/PHASE_1_BRIEF.md +7 -7
  37. package/package.json +1 -1
  38. package/scripts/forgetrail-cli.mjs +1 -1
  39. package/scripts/install-lib.mjs +1 -0
  40. package/scripts/install.mjs +6 -23
  41. package/scripts/publish-gate.mjs +6 -4
  42. package/scripts/tracking-cutover.test.mjs +109 -0
  43. package/scripts/workspace-index.mjs +9 -1
@@ -17,7 +17,6 @@ import {
17
17
  const ROOT_FILES = [
18
18
  "WORKFLOW.md",
19
19
  "TRACKING_SCHEMA.md",
20
- "workflow_tracking.json",
21
20
  "INITIAL_PROMPT.md",
22
21
  "CONTINUATION_PROMPT.md",
23
22
  "LICENSE",
@@ -70,14 +69,6 @@ export function runInstallForgetrail(rawArgv, { defaultToCwd = false } = {}) {
70
69
  copyPath(join(FORGETRAIL_ROOT, "prompts"), join(forgetrailDir, "prompts"), opts);
71
70
  copyContentDir(contentSrc, contentDest, opts);
72
71
 
73
- if (!args.skipTracking) {
74
- copyPath(
75
- join(FORGETRAIL_ROOT, "workflow_tracking.json"),
76
- join(target, ".forgetrail", "workflow_tracking.json"),
77
- { ...opts, force: args.force }
78
- );
79
- }
80
-
81
72
  const hooksSrc = join(FORGETRAIL_ROOT, "content", "hooks");
82
73
  const hooksDest = join(target, ".forgetrail", "hooks");
83
74
  ensureDir(hooksDest, args.dryRun);
@@ -95,7 +86,7 @@ export function runInstallForgetrail(rawArgv, { defaultToCwd = false } = {}) {
95
86
  console.log("\nDone.");
96
87
  console.log(" Methodology: _forgetrail/WORKFLOW.md");
97
88
  console.log(" First chat: _forgetrail/INITIAL_PROMPT.md");
98
- console.log(" Tracking: .forgetrail/workflow_tracking.json");
89
+ console.log(" Project record: appledger/ (not workflow_tracking.json)");
99
90
  console.log(" Hooks: .forgetrail/hooks/ (enforced via .cursor/hooks.json)");
100
91
  return { target, mode: "full" };
101
92
  }
@@ -127,14 +118,6 @@ export function runInstallLite(rawArgv, { defaultToCwd = false } = {}) {
127
118
  );
128
119
  }
129
120
 
130
- if (!args.skipTracking) {
131
- copyPath(
132
- join(contentDir, "LITE_WORKFLOW_TRACKING.json"),
133
- join(forgetrailDir, "workflow_tracking.json"),
134
- opts
135
- );
136
- }
137
-
138
121
  const hooksSrc = join(contentDir, "hooks");
139
122
  const hooksDest = join(forgetrailDir, "hooks");
140
123
  ensureDir(hooksDest, args.dryRun);
@@ -158,7 +141,7 @@ export function runInstallLite(rawArgv, { defaultToCwd = false } = {}) {
158
141
 
159
142
  console.log("\nDone.");
160
143
  console.log(" Protocol: .forgetrail/FORGETRAIL_LITE.md");
161
- console.log(" Tracking: .forgetrail/workflow_tracking.json (lite-1 schema)");
144
+ console.log(" Project record: appledger/ (a tracking JSON file is not installed)");
162
145
  console.log(" Hooks: .forgetrail/hooks/ (enforced via .cursor/hooks.json)");
163
146
  if (args.withGenesisStub) {
164
147
  console.log(" Genesis: docs/GENESIS.md (stub — replace with your spec)");
@@ -177,12 +160,12 @@ Usage:
177
160
  Options:
178
161
  --path, -p <dir> Target project root (default: current directory for \`forgetrail\` CLI)
179
162
  --force, -f Overwrite existing files
180
- --skip-tracking Do not create .forgetrail/workflow_tracking.json
163
+ --skip-tracking Accepted and ignored. A tracking JSON file is not installed.
181
164
  --dry-run Print actions only
182
165
 
183
166
  Installs:
184
167
  <target>/_forgetrail/ WORKFLOW, templates, prompts, content/ (curated)
185
- <target>/.forgetrail/ workflow_tracking.json starter (unless skipped)
168
+ <target>/.forgetrail/ hooks only. Project state belongs in appledger/.
186
169
  `.trim();
187
170
 
188
171
  export const INSTALL_LITE_HELP = `
@@ -195,11 +178,11 @@ Usage:
195
178
  Options:
196
179
  --path, -p <dir> Target project root (default: current directory for \`forgetrail\` CLI)
197
180
  --force, -f Overwrite existing files
198
- --skip-tracking Do not create workflow_tracking.json starter
181
+ --skip-tracking Accepted and ignored. A tracking JSON file is not installed.
199
182
  --with-genesis-stub Also create docs/GENESIS.md stub (for the Try path)
200
183
  --dry-run Print actions only
201
184
 
202
- Installs into <target>/.forgetrail/ (FORGETRAIL_LITE.md, tracking starter, cursor rules).
185
+ Installs into <target>/.forgetrail/ (FORGETRAIL_LITE.md, cursor rules, hooks). Project state belongs in appledger/.
203
186
  With --with-genesis-stub, also writes docs/GENESIS.md from content/GENESIS_STUB.md.
204
187
  See TRY_FORGETRAIL.md in the ForgeTrail repo for the no-MCP prove-it recipe.
205
188
  `.trim();
@@ -98,11 +98,13 @@ for (const needed of ["WORKFLOW.md", "README.md", "LICENSE", "content/FORGETRAIL
98
98
  console.log("\n✓ forgetrail pack check passed");
99
99
  console.log(` forgetrail@${root.version}`);
100
100
  console.log(` forgetrail-mcp@${mcp.version} (build ok at ${MCP_ENTRY})`);
101
- console.log("\nPublish (you run these):");
102
- console.log(" pnpm publish --access public");
103
- console.log(" pnpm --dir mcp-server publish --access public");
104
- console.log("\nDry-run first if you want:");
101
+ console.log("\nPublish (you run these, from a clean tree, after pushing if the tarball should match GitHub):");
105
102
  console.log(" pnpm publish --dry-run --access public");
103
+ console.log(" pnpm publish --access public");
104
+ console.log(" cd mcp-server");
105
+ console.log(" pnpm publish --access public");
106
+ console.log(" cd ..");
107
+ console.log("Do not use pnpm --dir mcp-server publish. npm 12 treats leftover --dir as an extra package spec.");
106
108
 
107
109
  const here = fileURLToPath(import.meta.url);
108
110
  if (resolve(process.argv[1] ?? "") === resolve(here)) {
@@ -0,0 +1,109 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import test from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { validateTrackingData } from "../content/hooks/validate-tracking-core.mjs";
9
+
10
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
11
+
12
+ test("a pointer is not a second decision log", () => {
13
+ const result = validateTrackingData({
14
+ record: "appledger/",
15
+ status: "pointer",
16
+ note: "Decisions live in appledger/.",
17
+ });
18
+ assert.equal(result.issues.length, 0);
19
+ assert.match(result.warnings.join("\n"), /pointer to appledger/);
20
+ });
21
+
22
+ test("a legacy tracking document is a conflict", () => {
23
+ const result = validateTrackingData({
24
+ project: { name: "Notes", description: "Find notes" },
25
+ currentPhase: "4-feature-iteration",
26
+ phases: { "4-feature-iteration": { status: "in_progress", exitCriteriaRemaining: ["Edge cases"] } },
27
+ decisions: [{ decision: "Use local files" }],
28
+ sessions: [],
29
+ });
30
+ assert.match(result.issues.join("\n"), /not the system of record/);
31
+ assert.match(result.issues.join("\n"), /appledger migrate preview/);
32
+ });
33
+
34
+ test("install does not create workflow_tracking.json", () => {
35
+ for (const args of [
36
+ ["install", "--lite", "--dry-run"],
37
+ ["install", "--dry-run"],
38
+ ]) {
39
+ const dir = mkdtempSync(join(tmpdir(), "ft-cutover-"));
40
+ const result = spawnSync(process.execPath, ["scripts/forgetrail-cli.mjs", ...args, "--path", dir], {
41
+ cwd: root,
42
+ encoding: "utf8",
43
+ });
44
+ assert.equal(result.status, 0, result.stderr || result.stdout);
45
+ assert.doesNotMatch(result.stdout, /\\workflow_tracking\.json/);
46
+ assert.match(result.stdout, /Project record: appledger/);
47
+ }
48
+ });
49
+
50
+ test("session start warns on a legacy file and does not quote it as the live phase", () => {
51
+ const dir = mkdtempSync(join(tmpdir(), "ft-session-"));
52
+ mkdirSync(join(dir, ".forgetrail"), { recursive: true });
53
+ writeFileSync(
54
+ join(dir, ".forgetrail", "workflow_tracking.json"),
55
+ JSON.stringify({
56
+ project: { name: "Notes" },
57
+ currentPhase: "4-feature-iteration",
58
+ phases: {},
59
+ decisions: [{ decision: "Keep the old log" }],
60
+ })
61
+ );
62
+ const result = spawnSync(process.execPath, [join(root, "content", "hooks", "session-start.mjs")], {
63
+ cwd: dir,
64
+ encoding: "utf8",
65
+ });
66
+ assert.equal(result.status, 0, result.stderr);
67
+ const payload = JSON.parse(result.stdout);
68
+ assert.match(payload.additional_context, /not the system of record/);
69
+ assert.doesNotMatch(payload.additional_context, /Current Phase: 4-feature-iteration/);
70
+ });
71
+
72
+ test("Lite and workflow instructions name the ledger", () => {
73
+ const lite = readFileSync(join(root, "content", "FORGETRAIL_LITE.md"), "utf8");
74
+ const legacyAt = lite.indexOf("## 11. Legacy");
75
+ const resumeAt = lite.indexOf("## 12. `AGENTS.md`");
76
+ assert.ok(legacyAt > 0 && resumeAt > legacyAt);
77
+ const instructional = `${lite.slice(0, legacyAt)}\n${lite.slice(resumeAt)}`;
78
+ assert.match(lite.slice(legacyAt, resumeAt), /"schemaVersion": "lite-1"/);
79
+ assert.match(instructional, /ForgeTrail Lite v2\.2\.0/);
80
+ for (const line of instructional.split("\n")) {
81
+ if (!line.includes("workflow_tracking.json")) continue;
82
+ assert.match(line, /[Dd]o not|does not|not the live phase|not recreate/, line);
83
+ }
84
+ const workflow = readFileSync(join(root, "WORKFLOW.md"), "utf8");
85
+ assert.match(workflow, /not_applicable/);
86
+ assert.match(workflow, /data\.companion_outcomes/);
87
+ assert.doesNotMatch(workflow, /Where this file names that JSON/);
88
+ for (const line of workflow.split("\n")) {
89
+ if (!line.includes("workflow_tracking.json")) continue;
90
+ assert.match(line, /[Dd]o not|does not|not the system of record|not the live phase/, line);
91
+ }
92
+ });
93
+
94
+ test("kickoff names appledger init and does not require the tracking file", () => {
95
+ const kickoff = readFileSync(join(root, "mcp-server", "src", "index.ts"), "utf8");
96
+ assert.match(kickoff, /appledger init/);
97
+ assert.doesNotMatch(kickoff, /no separate init command/);
98
+ const plan = readFileSync(join(root, "content", "PLAN_MODE_PATTERNS.md"), "utf8");
99
+ assert.match(plan, /decision record/);
100
+ assert.doesNotMatch(plan, /decisions\[\]/);
101
+ assert.match(plan, /Do not create `\.forgetrail\/workflow_tracking\.json`/);
102
+ const genesis = readFileSync(join(root, "content", "GENESIS_STUB.md"), "utf8");
103
+ assert.match(genesis, /appledger init/);
104
+ assert.doesNotMatch(genesis, /Create `\.forgetrail\/workflow_tracking\.json`/);
105
+ const intake = readFileSync(join(root, "content", "GREENFIELD_INTAKE.md"), "utf8");
106
+ assert.doesNotMatch(intake, /tracking file/);
107
+ assert.doesNotMatch(intake, /decisions\[\]/);
108
+ assert.match(intake, /archetype/);
109
+ });
@@ -40,8 +40,9 @@ function scanWorkspace(workspaceRoot) {
40
40
 
41
41
  const trackingPath = findTracking(fullPath);
42
42
  const hooksStatus = hasHooks(fullPath);
43
+ const ledger = existsSync(join(fullPath, "appledger", "manifest.yaml"));
43
44
 
44
- if (!trackingPath && hooksStatus === "none") {
45
+ if (!trackingPath && !ledger && hooksStatus === "none") {
45
46
  continue;
46
47
  }
47
48
 
@@ -77,9 +78,16 @@ function scanWorkspace(workspaceRoot) {
77
78
  openCriteriaCount = pObj.exitCriteriaRemaining.length;
78
79
  }
79
80
  }
81
+ if (tracking.status === "pointer") {
82
+ phase = ledger ? "appledger" : "pointer";
83
+ } else if (tracking.currentPhase || tracking.phases) {
84
+ phase = ledger ? `legacy+${phase}` : `legacy:${phase}`;
85
+ }
80
86
  } catch {}
81
87
  }
82
88
 
89
+ if (ledger && phase === "-") phase = "appledger";
90
+
83
91
  rows.push({
84
92
  repo: entry,
85
93
  name: projectName,