forgetrail 0.5.7 → 0.5.9

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.
@@ -5,167 +5,9 @@ Phase, decisions, sessions, and gotchas live in **`appledger/`**.
5
5
  - **`appledger/profiles/forgetrail.yaml`** holds the current phase instance and criteria. Phase ids are `plan`, `build`, `stabilize`, `iterate`, `refine`, `align`, and `harden`.
6
6
  - Decision records, session records, and lesson or question records live under **`appledger/records/`**.
7
7
  - Do **not** create or update **`.forgetrail/workflow_tracking.json`**. `.forgetrail/` is hooks and host integration.
8
- - If that JSON file already exists and its `status` is not `pointer`, run **`appledger migrate preview`**, then apply once. Do not keep a second decision log.
8
+ - If that JSON file already exists and its `status` is not `pointer`, preserve it; current migration is retired. Historical recovery is documented at https://appledger.dev/docs/migration-retirement. Do not keep a second decision log.
9
9
  - A file with `"status": "pointer"` and `"record": "appledger/"` is a leftover pointer. Do not add decisions, sessions, or phase status to it.
10
10
 
11
11
  `getInitialWorkflowTracking` does not return a starter JSON file. `validateTracking` classifies an existing file. It does not accept a legacy document as healthy writable state.
12
12
 
13
- The rest of this file describes the **legacy** Lite and full JSON shapes so an existing file can be recognized and migrated. It is not a template to copy into a new project.
14
-
15
- ## Legacy phase id vocabulary (Lite vs full)
16
-
17
- Both legacy schemas describe the same seven lifecycle phases. **`scripts/forgetrail-dev-launcher.mjs`** can still read either shape. New work uses the profile ids in the table's display-name column.
18
-
19
-
20
- | # | Lite `currentPhase` | MCP `currentPhase` | Display name |
21
- |---|---------------------|--------------------|--------------|
22
- | 1 | `1` | `1-architecture` | Plan |
23
- | 2 | `2` | `2-scaffolding` | Build |
24
- | 3 | `3` | `3-stabilization` | Stabilize |
25
- | 4 | `4` | `4-feature-iteration` | Iterate |
26
- | 5 | `5` | `5-refactoring` | Refine |
27
- | 6 | `6` | `6-strategic-review` | Align |
28
- | 7 | `7` | `7-hardening` | Harden |
29
-
30
- **Lite (`schemaVersion: "lite-1"`):** numeric `currentPhase`; each phase has **`exitCriteria`** as `{ "flagName": true/false }`. The old copy block is **`FORGETRAIL_LITE.md`** §11. Do not write it into a project.
31
-
32
- **Full / MCP:** string `currentPhase`; each phase has **`exitCriteriaMet`** and **`exitCriteriaRemaining`** string arrays. The old sample remains at repo-root **`workflow_tracking.json`** for migration only. `getInitialWorkflowTracking` does not return it.
33
-
34
- ## Top-Level Fields
35
-
36
- ### `project`
37
-
38
- Static metadata. Set once at project creation (except `status`, which flips at wrap).
39
-
40
- ```json
41
- {
42
- "name": "My App",
43
- "created": "2026-03-12",
44
- "description": "One-sentence description",
45
- "archetype": "product",
46
- "status": "active"
47
- }
48
- ```
49
-
50
- - **`archetype`** — `product` (default) | `internal-tool` | `one-shot`. Set during Phase 1 per **WORKFLOW.md §1d**. When the archetype is not `product`, **prune** non-applicable exit criteria from the phase arrays at bootstrap (e.g. drop payments/business-plan criteria for an internal tool; collapse phases 5–7 into a single polish-and-ship gate for a one-shot). Log the pruning in `decisions[]` so the removal reads as intentional. Do **not** leave inapplicable criteria in place and mark them "N/A" one by one — prune once instead.
51
- - **`status`** — `active` (default) | `wrapped`. Set to `wrapped` when the project is finished or shelved, after running the **wrap protocol** (WORKFLOW.md §1e): sweep `gotchas[]` + `decisions[]` for generalizable lessons, run the propagation prompt (Harvest mode), add a final `sessions[]` entry with end state and handoff pointers.
52
-
53
- ### `currentPhase`
54
-
55
- String. One of: `1-architecture`, `2-scaffolding`, `3-stabilization`, `4-feature-iteration`, `5-refactoring`, `6-strategic-review`, `7-hardening`.
56
-
57
- Note: Phases 4 and 5 can alternate. A project might go 4 -> 5 -> 4 -> 6 -> 4 -> 7. The tracking file reflects reality, not a strict linear path.
58
-
59
- ### `phases`
60
-
61
- Each phase has:
62
-
63
- - `status`: `not_started` | `in_progress` | `completed` | `revisiting` (for phases you return to)
64
- - `startedAt` / `completedAt`: ISO timestamps
65
- - `exitCriteriaMet`: Array of strings (criteria that have been satisfied)
66
- - `exitCriteriaRemaining`: Array of strings (criteria not yet met)
67
- - `notes`: Array of `{ timestamp, text }` objects for anything worth recording during this phase
68
-
69
- When Claude believes exit criteria are met, it moves items from `exitCriteriaRemaining` to `exitCriteriaMet` and proposes the phase transition.
70
-
71
- ### `decisions`
72
-
73
- Architectural and design decisions with rationale. **Start logging in Phase 1** alongside **`docs/PHASE_1_BRIEF.md`** so Phase 2 can begin without the Phase 1 chat. After Phase 2, this array stays aligned with **Key Architectural Decisions** in `CONTEXT_PROMPT.md` (which should be populated by merging the brief).
74
-
75
- ```json
76
- {
77
- "id": "d1",
78
- "timestamp": "2026-03-12T10:30:00",
79
- "phase": "1-architecture",
80
- "session": 1,
81
- "decision": "Use PocketBase instead of filesystem storage",
82
- "rationale": "Need multi-user support and real-time sync. Filesystem won't scale.",
83
- "alternatives_considered": ["SQLite", "Supabase", "No database"],
84
- "status": "active"
85
- }
86
- ```
87
-
88
- `status` can be `active`, `superseded` (replaced by a later decision), or `revisit` (flagged for reconsideration). If superseded, add `superseded_by` field with the ID of the replacement decision.
89
-
90
- ### `gotchas`
91
-
92
- Issues, surprises, or traps encountered during development. These persist across sessions so the same mistake isn't repeated.
93
-
94
- ```json
95
- {
96
- "timestamp": "2026-03-12T14:00:00",
97
- "phase": "3-stabilization",
98
- "session": 1,
99
- "issue": "SvelteKit .env doesn't populate process.env",
100
- "resolution": "Use $env/dynamic/private instead",
101
- "category": "environment"
102
- }
103
- ```
104
-
105
- Categories: `environment`, `integration`, `data-model`, `ui`, `performance`, `ai-output`, `security`, `tooling`, `other`.
106
-
107
- ### `sessions`
108
-
109
- Log of each session with what was accomplished and where it left off.
110
-
111
- ```json
112
- {
113
- "session": 1,
114
- "date": "2026-03-12",
115
- "phasesWorked": ["1-architecture", "2-scaffolding"],
116
- "accomplished": ["Confirmed tech stack", "Built full skeleton", "Imported 44 existing jobs"],
117
- "leftOff": "App runs but env vars need fixing. Phase 3 next.",
118
- "contextLimitHit": false,
119
- "approximateLines": 3500
120
- }
121
- ```
122
-
123
- ## Update Rules for Claude
124
-
125
- 1. **After completing a task:** Update the relevant phase's `notes` and move any satisfied exit criteria.
126
- 2. **After making a decision:** Add to `decisions` array with full rationale.
127
- 3. **After hitting a gotcha:** Add to `gotchas` array immediately so it's captured.
128
- 4. **At session end:** Add a session entry to `sessions` array.
129
- 5. **At phase transition:** Update `currentPhase`, set `completedAt` on the old phase, `startedAt` on the new one.
130
- 6. **When revisiting a phase:** Set status to `revisiting`, don't overwrite the original `completedAt`.
131
-
132
- ## Custom Extensions
133
-
134
- Projects may add custom fields to phases as needed. For example, `4-feature-iteration` has an `iterations` array for tracking feature-by-feature progress:
135
-
136
- ```json
137
- {
138
- "feature": "DOCX tailoring",
139
- "status": "complete",
140
- "turnsSpent": 8,
141
- "approach": "Programmatic placeholder replacement",
142
- "notes": "LLM find/replace failed due to Word XML run splitting. Pivoted in turn 6."
143
- }
144
- ```
145
-
146
- Add whatever fields make sense for your project. The schema is a starting point, not a cage.
147
-
148
- ### `agentContext` (optional, top-level)
149
-
150
- Record which agent stack drives the project and what native primitives are available:
151
-
152
- ```json
153
- {
154
- "primaryAgent": "cursor-agent",
155
- "supportsSubagents": true,
156
- "supportsPlanMode": true,
157
- "lastSessionId": "optional-host-session-id"
158
- }
159
- ```
160
-
161
- ### `subagentRuns` (optional, per phase)
162
-
163
- Optional array under a phase object to log parallel subagent work (especially Phase 4, 5, 7):
164
-
165
- ```json
166
- {
167
- "id": "sa-7-security",
168
- "description": "Black-hat audit via read-only subagent",
169
- "outcome": "12 findings; 2 critical — synthesized into BLACK_HAT_REPORT.md"
170
- }
171
- ```
13
+ The legacy shapes and starter are retained in ForgeTrail Git history at commit `3863f68e01d6f57823ffbc2a94b626defeef9bfe`. Current hooks and MCP only reject a writable legacy file or identify an inert pointer; they do not validate or translate the retired format.
@@ -31,7 +31,7 @@ If your host supports hooks (Cursor `hooks.json`, Claude Code `settings.json`),
31
31
 
32
32
  ## 1. How to use this file
33
33
 
34
- **Humans (first time):** Prefer the repo-root recipe **[TRY_FORGETRAIL.md](../TRY_FORGETRAIL.md)**: Genesis spec in any chat app, then this Lite file, then one kickoff line to your coding agent. No MCP required.
34
+ **Humans (first time):** Prefer the upstream ForgeTrail recipe **[TRY_FORGETRAIL.md](https://github.com/Catalyst-Forge-LLC/forgetrail/blob/main/TRY_FORGETRAIL.md)**: Genesis spec in any chat app, then this Lite file, then one kickoff line to your coding agent. The recipe lives in the upstream repository; Lite installation does not place it in your app root. No MCP required.
35
35
 
36
36
  **Option A — drop-in file (preferred).** Save this doc in **`.forgetrail/FORGETRAIL_LITE.md`** (see §1.5). You may commit `.forgetrail/` (same Apache 2.0 license) or gitignore it for a cleaner public app history — see §1.5. Most agents will read it when cited (e.g. *"follow `.forgetrail/FORGETRAIL_LITE.md`"*). Then save the **AGENTS.md snippet** from §12 as **`.forgetrail/AGENTS.md`**. Symlink or copy `.forgetrail/cursor/rules/*.mdc` into `.cursor/rules/` so Cursor loads them (§1.5). If you already have a product idea, pair this file with **`docs/GENESIS.md`** (see TRY_FORGETRAIL.md and `content/GENESIS_SPEC_PROMPT.md`).
37
37
 
@@ -945,55 +945,9 @@ Keep it **short** — this is a map, not a monograph. If it grows past ~200 line
945
945
 
946
946
  ---
947
947
 
948
- ## 11. Legacy `.forgetrail/workflow_tracking.json` (do not copy)
948
+ ## 11. AppLedger record
949
949
 
950
- Do **not** write this into a project. It is the old Lite shape. `appledger migrate` reads it when a project already has the file. New projects start at `appledger/`.
951
-
952
- **Lite vs MCP tracking:** Lite uses **`schemaVersion: "lite-1"`**, numeric **`currentPhase`** (`1`–`7`), and per-phase **`exitCriteria`** objects (boolean flags). The full MCP starter uses string phase keys (`1-architecture`, …) and **`exitCriteriaMet`** / **`exitCriteriaRemaining`** arrays — see **`TRACKING_SCHEMA.md`** (phase ID map + both shapes). **`scripts/forgetrail-dev-launcher.mjs`** `status` understands both.
953
-
954
- ```json
955
- {
956
- "schemaVersion": "lite-1",
957
- "project": {
958
- "name": "<project name>",
959
- "created": "<YYYY-MM-DD>",
960
- "description": "<one-line description>",
961
- "sourceControl": "git",
962
- "archetype": "product",
963
- "status": "active"
964
- },
965
- "currentPhase": 1,
966
- "phases": {
967
- "1": { "name": "Plan", "status": "in_progress", "exitCriteria": { "phase1BriefLocked": false, "stackLocked": false, "heroFlowAgreed": false, "v1ScopeAgreed": false } },
968
- "2": { "name": "Build", "status": "pending", "exitCriteria": { "runnableSpine": false, "heroFlowEndToEnd": false, "contextPromptWritten": false } },
969
- "3": { "name": "Stabilize", "status": "pending", "exitCriteria": {} },
970
- "4": { "name": "Iterate", "status": "pending", "exitCriteria": {} },
971
- "5": { "name": "Refine", "status": "pending", "exitCriteria": {} },
972
- "6": { "name": "Align", "status": "pending", "exitCriteria": {} },
973
- "7": { "name": "Harden", "status": "pending", "exitCriteria": {} }
974
- },
975
- "decisions": [],
976
- "gotchas": [],
977
- "sessions": [],
978
- "openQuestions": []
979
- }
980
- ```
981
-
982
- **Entry shapes** (append-only; include **`phase`** when the decision belongs to a specific lifecycle phase):
983
-
984
- ```json
985
- // decisions[] — date, phase, decision, why, alternatives (alternatives may be [])
986
- { "date": "2026-04-22", "phase": 1, "decision": "Stack: SvelteKit + PocketBase", "why": "single-binary DB, fast spine", "alternatives": ["Next+Postgres"] }
987
-
988
- // gotchas[]
989
- { "date": "2026-04-22", "phase": 2, "gotcha": "sv create hung: Directory not empty. Continue?", "fix": "Lite root is never empty by step 10; use manual scaffold (§4.2 step 10 A.1) or sv create into a new empty subfolder (app/); kill stuck process" }
990
- { "date": "2026-04-22", "phase": 2, "gotcha": "sv create hangs on Tailwind plugin prompt", "fix": "pass --no-add-ons or explicit --tailwindcss=plugins:none" }
991
- { "date": "2026-04-22", "phase": 2, "gotcha": "port 8090 already in use by background process", "fix": "configured PUBLIC_POCKETBASE_URL with port 8096 in .env" }
992
- { "date": "2026-04-23", "phase": 2, "gotcha": "tar -xf on Windows: (a) 'Cannot connect to Z: resolve failed' — bsdtar parses drive letters as remote host; (b) 'This does not look like a tar archive' — GNU tar from Git Bash shadowed bsdtar on PATH", "fix": "On win32 branch: PowerShell Expand-Archive (or C:\\Windows\\System32\\tar.exe by absolute path) with cwd + basename, not full X:\\... path; see §13 Engineering" }
993
-
994
- // sessions[]
995
- { "date": "2026-04-22", "phase": 2, "summary": "wired auth + hero route end-to-end", "nextSession": "seed data + first export" }
996
- ```
950
+ Project phase, decisions, lessons, and sessions live in `appledger/`. Read its manifest, the ForgeTrail profile, and the latest session. Do not create a legacy `workflow_tracking.json`. The old tracking shapes are retained in Git history, not shipped as starter templates. Historical recovery is documented at https://appledger.dev/docs/migration-retirement.
997
951
 
998
952
  ---
999
953
 
@@ -5,7 +5,7 @@ alwaysApply: true
5
5
 
6
6
  # ForgeTrail phase awareness
7
7
 
8
- This repo uses **ForgeTrail**. **Source of truth:** `appledger/profiles/forgetrail.yaml` and the latest session record. If a legacy `.forgetrail/workflow_tracking.json` exists and is not a pointer, it is a legacy conflict: run `appledger migrate preview`, then apply.
8
+ This repo uses **ForgeTrail**. **Source of truth:** `appledger/profiles/forgetrail.yaml` and the latest session record. If a legacy `.forgetrail/workflow_tracking.json` exists and is not a pointer, it is a legacy conflict: preserve the file and follow the historical recovery procedure at https://appledger.dev/docs/migration-retirement. Current migration is retired.
9
9
 
10
10
  ## When to surface status
11
11
 
@@ -11,7 +11,7 @@ Used for **both** ForgeTrail Lite file bootstrap and **MCP greenfield** kickoff.
11
11
  | `hooks/` | Host safety hooks. They do not store project phase or decisions |
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
- | `AGENTS.md` | Agent instructions — cite or symlink for your IDE |
14
+ | `AGENTS.md` | Created by the agent in the first kickoff session from Lite §12; absent immediately after installation. Cite or symlink it for your IDE after creation |
15
15
  | `CLAUDE.md` | Claude Code instructions |
16
16
  | `IDEAS.md` | Backlog parking lot |
17
17
  | `cursor/rules/*.mdc` | Cursor rule snippets (updates-log, …) |
@@ -34,8 +34,8 @@ mkdir .cursor\rules 2>nul
34
34
  mklink .cursor\rules\forgetrail-updates-log.mdc ..\.forgetrail\cursor\rules\forgetrail-updates-log.mdc
35
35
  ```
36
36
 
37
- Optional: symlink `.forgetrail/AGENTS.md` → `AGENTS.md` at repo root if a tool requires root placement (also gitignore root copies if you symlink).
37
+ After the first kickoff creates `.forgetrail/AGENTS.md` from Lite §12, optionally symlink it → `AGENTS.md` at repo root if a tool requires root placement (also gitignore root copies if you symlink).
38
38
 
39
39
  ## Recovering after clone
40
40
 
41
- If `.forgetrail/` was gitignored, it is not in git. Copy from upstream Catalyst Forge (`content/`) or restore from backup; MCP-only projects can recreate tracking via **`getInitialWorkflowTracking`**.
41
+ If `.forgetrail/` was gitignored, it is not in git. Reinstall ForgeTrail, copy its host artifacts from upstream Catalyst Forge (`content/`), or restore them from backup. Restore any project-specific agent instructions from backup or recreate them during kickoff. The existing **`appledger/`** remains the authoritative project record; restoring host artifacts does not replace it. For a new empty project, use **`appledger init`**. MCP **`getInitialWorkflowTracking`** returns ledger guidance, not a tracking file or a replacement ledger.
@@ -74,7 +74,7 @@ function main() {
74
74
  lines.push("=== Legacy tracking conflict ===");
75
75
  lines.push("workflow_tracking.json is not the system of record.");
76
76
  lines.push("Do not add decisions, sessions, or phase status to it.");
77
- lines.push("Run `appledger migrate preview`, then apply once, and write only in appledger/.");
77
+ lines.push("Legacy migration is retired. Preserve the legacy file; use an isolated copy and the historical AppLedger revision for recovery. See https://appledger.dev/docs/migration-retirement.");
78
78
  }
79
79
  } catch {
80
80
  lines.push("workflow_tracking.json could not be read. Do not recreate it. Use appledger/.");
@@ -125,7 +125,7 @@ function main() {
125
125
  console.log(
126
126
  JSON.stringify({
127
127
  followup_message:
128
- "ForgeTrail reminder: workflow_tracking.json is a legacy file. Do not append sessions to it. Run `appledger migrate preview`, then apply, and record this session in appledger/.",
128
+ "ForgeTrail reminder: workflow_tracking.json is a legacy file. Do not append sessions to it. Legacy migration is retired. Preserve the legacy file; recover from the historical AppLedger revision in an isolated copy. Record current sessions in appledger/.",
129
129
  })
130
130
  );
131
131
  return;
@@ -1,262 +1,20 @@
1
- /**
2
- * Structural validation for .forgetrail/workflow_tracking.json (MCP full + Lite shapes).
3
- * Standalone zero-dependency module for ForgeTrail hooks and MCP.
4
- */
5
-
6
- const VALID_MCP_PHASES = [
7
- "1-architecture",
8
- "2-scaffolding",
9
- "3-stabilization",
10
- "4-feature-iteration",
11
- "5-refactoring",
12
- "6-strategic-review",
13
- "7-hardening",
14
- ];
15
-
16
- const VALID_LITE_PHASES = ["1", "2", "3", "4", "5", "6", "7"];
17
-
18
- const VALID_GOTCHA_CATEGORIES = new Set([
19
- "environment",
20
- "integration",
21
- "data-model",
22
- "ui",
23
- "performance",
24
- "ai-output",
25
- "security",
26
- "tooling",
27
- "other",
28
- ]);
29
-
30
- const VALID_DECISION_STATUS = new Set(["active", "superseded", "revisit"]);
31
-
32
- const VALID_PHASE_STATUS = new Set([
33
- "not_started",
34
- "pending",
35
- "in_progress",
36
- "completed",
37
- "revisiting",
38
- ]);
39
-
40
- function isLiteSchema(data) {
41
- return data && data.schemaVersion === "lite-1";
42
- }
43
-
44
- const LEGACY_TRACKING_ISSUE =
45
- "Legacy writable tracking is not the system of record. Do not add decisions, sessions, or phase status to this file. Run `appledger migrate preview`, then apply once, and write only in appledger/.";
46
-
47
- const POINTER_WARNING =
48
- "This file is a pointer to appledger/. Do not add decisions, sessions, or phase status here.";
49
-
50
- function isPointer(data) {
51
- return data.status === "pointer" && typeof data.record === "string" && data.record.replaceAll("\\", "/").includes("appledger");
52
- }
53
-
54
- function isLegacyTracking(data) {
55
- return (
56
- "currentPhase" in data ||
57
- "phases" in data ||
58
- "decisions" in data ||
59
- "sessions" in data ||
60
- "schemaVersion" in data
61
- );
62
- }
63
-
1
+ /** Diagnose retired tracking files. Current project records live in appledger/. */
64
2
  export function validateTrackingData(data) {
65
- const issues = [];
66
- const warnings = [];
67
-
68
- if (!data || typeof data !== "object") {
69
- issues.push("Tracking data is empty or not an object.");
70
- return { issues, warnings };
71
- }
72
-
73
- if (isPointer(data)) {
74
- warnings.push(POINTER_WARNING);
75
- return { issues, warnings };
76
- }
77
-
78
- if (isLegacyTracking(data)) {
79
- issues.push(LEGACY_TRACKING_ISSUE);
3
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
4
+ return { issues: ['Tracking data is empty or not an object.'], warnings: [] };
80
5
  }
81
-
82
- const project = data.project;
83
- if (!project || typeof project !== "object") {
84
- issues.push("Missing top-level project object.");
85
- } else if (!project.name || typeof project.name !== "string" || !project.name.trim()) {
86
- issues.push("project.name is missing or empty.");
87
- }
88
-
89
- if (!data.currentPhase) {
90
- issues.push("Missing currentPhase.");
91
- }
92
-
93
- const lite = isLiteSchema(data);
94
- const currentPhase = data.currentPhase;
95
-
96
- if (currentPhase) {
97
- if (lite) {
98
- const phaseStr = String(currentPhase);
99
- if (!VALID_LITE_PHASES.includes(phaseStr)) {
100
- issues.push(
101
- `Lite schema currentPhase "${phaseStr}" is not a valid phase number (1–7).`
102
- );
103
- }
104
- } else if (
105
- typeof currentPhase === "string" &&
106
- !VALID_MCP_PHASES.includes(currentPhase)
107
- ) {
108
- issues.push(
109
- `currentPhase "${currentPhase}" is not a recognized MCP phase id (e.g. "1-architecture").`
110
- );
111
- }
6
+ if (data.status === 'pointer' && typeof data.record === 'string' && data.record.replaceAll('\\', '/').includes('appledger')) {
7
+ return { issues: [], warnings: ["This file is a pointer to appledger/. Do not add decisions, sessions, or phase status here."] };
112
8
  }
113
-
114
- const phases = data.phases;
115
- if (!phases || typeof phases !== "object") {
116
- issues.push("Missing phases object.");
117
- return { issues, warnings };
118
- }
119
-
120
- const phaseKeys = Object.keys(phases);
121
- if (phaseKeys.length === 0) {
122
- issues.push("No phases defined in tracking.");
123
- }
124
-
125
- if (!lite && currentPhase && typeof currentPhase === "string" && !phaseKeys.includes(currentPhase)) {
126
- issues.push(`currentPhase "${currentPhase}" is not a key in phases.`);
127
- }
128
-
129
- for (const [key, rawPhase] of Object.entries(phases)) {
130
- if (!rawPhase || typeof rawPhase !== "object") {
131
- issues.push(`phases["${key}"] is not an object.`);
132
- continue;
133
- }
134
- const phase = rawPhase;
135
-
136
- if (phase.status && !VALID_PHASE_STATUS.has(String(phase.status))) {
137
- warnings.push(
138
- `phases["${key}"].status "${phase.status}" is not a standard value (not_started | in_progress | completed | revisiting).`
139
- );
140
- }
141
-
142
- if (lite) {
143
- if (phase.exitCriteria !== undefined && (typeof phase.exitCriteria !== "object" || phase.exitCriteria === null)) {
144
- issues.push(`phases["${key}"].exitCriteria should be an object of boolean flags (Lite schema).`);
145
- }
146
- } else {
147
- if (phase.exitCriteriaMet !== undefined && !Array.isArray(phase.exitCriteriaMet)) {
148
- issues.push(`phases["${key}"].exitCriteriaMet should be an array.`);
149
- }
150
- if (phase.exitCriteriaRemaining !== undefined && !Array.isArray(phase.exitCriteriaRemaining)) {
151
- issues.push(`phases["${key}"].exitCriteriaRemaining should be an array.`);
152
- }
153
- const met = phase.exitCriteriaMet ?? [];
154
- const remaining = phase.exitCriteriaRemaining ?? [];
155
- if (Array.isArray(met) && Array.isArray(remaining)) {
156
- const overlap = met.filter((item) => remaining.includes(item));
157
- if (overlap.length > 0) {
158
- warnings.push(
159
- `phases["${key}"] has exit criteria in both Met and Remaining: ${overlap.slice(0, 3).join("; ")}${overlap.length > 3 ? "…" : ""}`
160
- );
161
- }
162
- }
163
- }
164
-
165
- const subagentRuns = phase.subagentRuns;
166
- if (subagentRuns !== undefined && !Array.isArray(subagentRuns)) {
167
- issues.push(`phases["${key}"].subagentRuns should be an array when present.`);
168
- }
169
- }
170
-
171
- const decisions = data.decisions;
172
- if (decisions !== undefined) {
173
- if (!Array.isArray(decisions)) {
174
- issues.push("decisions should be an array.");
175
- } else {
176
- const seenIds = new Set();
177
- for (let i = 0; i < decisions.length; i++) {
178
- const d = decisions[i];
179
- if (!d || typeof d !== "object") {
180
- issues.push(`decisions[${i}] is not an object.`);
181
- continue;
182
- }
183
- if (!d.decision || typeof d.decision !== "string") {
184
- issues.push(`decisions[${i}] missing decision text.`);
185
- }
186
- if (!d.rationale || typeof d.rationale !== "string") {
187
- warnings.push(`decisions[${i}] missing rationale (recommended for Phase 1 handoff).`);
188
- }
189
- if (d.id && typeof d.id === "string") {
190
- if (seenIds.has(d.id)) {
191
- issues.push(`Duplicate decisions id "${d.id}".`);
192
- }
193
- seenIds.add(d.id);
194
- }
195
- if (d.status && !VALID_DECISION_STATUS.has(String(d.status))) {
196
- warnings.push(`decisions[${i}].status "${d.status}" is not standard (active | superseded | revisit).`);
197
- }
198
- if (
199
- d.alternatives_considered !== undefined &&
200
- !Array.isArray(d.alternatives_considered)
201
- ) {
202
- warnings.push(`decisions[${i}].alternatives_considered should be an array when present.`);
203
- }
204
- }
205
- }
206
- }
207
-
208
- const gotchas = data.gotchas;
209
- if (gotchas !== undefined) {
210
- if (!Array.isArray(gotchas)) {
211
- issues.push("gotchas should be an array.");
212
- } else {
213
- for (let i = 0; i < gotchas.length; i++) {
214
- const g = gotchas[i];
215
- if (!g || typeof g !== "object") {
216
- issues.push(`gotchas[${i}] is not an object.`);
217
- continue;
218
- }
219
- if (!g.issue || typeof g.issue !== "string") {
220
- warnings.push(`gotchas[${i}] missing issue description.`);
221
- }
222
- if (!g.resolution || typeof g.resolution !== "string") {
223
- warnings.push(`gotchas[${i}] missing resolution.`);
224
- }
225
- if (g.category && !VALID_GOTCHA_CATEGORIES.has(String(g.category))) {
226
- warnings.push(
227
- `gotchas[${i}].category "${g.category}" is not a standard category (environment, integration, …).`
228
- );
229
- }
230
- }
231
- }
232
- }
233
-
234
- const sessions = data.sessions;
235
- if (sessions !== undefined && !Array.isArray(sessions)) {
236
- issues.push("sessions should be an array when present.");
237
- }
238
-
239
- return { issues, warnings };
9
+ return { issues: ["Legacy writable tracking is not the system of record. Legacy migration is retired. Preserve the file; historical recovery uses an isolated copy and AppLedger commit fb7939c0f4d811930adb9cfd4bfd0c6766638a44. Write current decisions, sessions, and phase only in appledger/."], warnings: [] };
240
10
  }
241
11
 
242
12
  export function formatValidationResult(result) {
243
- const { issues, warnings } = result;
244
-
245
- if (issues.length === 0 && warnings.length === 0) {
246
- return "No workflow_tracking.json is required. Project state lives in appledger/.";
247
- }
248
-
249
- if (issues.length === 0 && warnings.every((item) => item.includes("pointer"))) {
250
- return "Pointer accepted. Project state lives in appledger/.\n\n" + warnings.map((item) => `- ${item}`).join("\n");
13
+ if (result.issues.length === 0 && result.warnings.length === 0) {
14
+ return 'No workflow_tracking.json is required. Project state lives in appledger/.';
251
15
  }
252
-
253
- const parts = [];
254
- if (issues.length > 0) {
255
- parts.push("Issues (fix these):\n" + issues.map((i) => `- ${i}`).join("\n"));
256
- }
257
- if (warnings.length > 0) {
258
- parts.push("Warnings:\n" + warnings.map((w) => `- ${w}`).join("\n"));
16
+ if (result.issues.length === 0) {
17
+ return 'Pointer accepted. Project state lives in appledger/.\n\n' + result.warnings.map(item => '- ' + item).join('\n');
259
18
  }
260
-
261
- return parts.join("\n\n");
19
+ return 'Issues (fix these):\n' + result.issues.map(item => '- ' + item).join('\n');
262
20
  }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Commands: setup | run | status
7
7
  */
8
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
8
+ import { existsSync, readFileSync } from "node:fs";
9
9
  import { join, dirname } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { spawn } from "node:child_process";
@@ -13,52 +13,8 @@ import { spawn } from "node:child_process";
13
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
14
  const repoRoot = join(__dirname, "..");
15
15
  const trackingPath = join(repoRoot, ".forgetrail", "workflow_tracking.json");
16
- const progressPath = join(repoRoot, "docs", "FORGETRAIL_PROGRESS.md");
17
16
  const isWin = process.platform === "win32";
18
17
 
19
- const PHASE_LABELS = {
20
- // Display names — see TRACKING_SCHEMA.md "Phase ID vocabulary (Lite vs MCP)"
21
- "1": "Plan",
22
- "2": "Build",
23
- "3": "Stabilize",
24
- "4": "Iterate",
25
- "5": "Refine",
26
- "6": "Align",
27
- "7": "Harden",
28
- "1-architecture": "Plan",
29
- "2-scaffolding": "Build",
30
- "3-stabilization": "Stabilize",
31
- "4-feature-iteration": "Iterate",
32
- "5-refactoring": "Refine",
33
- "6-strategic-review": "Align",
34
- "7-hardening": "Harden",
35
- };
36
-
37
- /** Lite schema: exitCriteria { key: boolean }. MCP schema: exitCriteriaMet / exitCriteriaRemaining arrays. */
38
- function getExitCriteriaStatus(phaseBlock) {
39
- if (!phaseBlock) return { met: [], remaining: [] };
40
-
41
- if (Array.isArray(phaseBlock.exitCriteriaMet) || Array.isArray(phaseBlock.exitCriteriaRemaining)) {
42
- return {
43
- met: Array.isArray(phaseBlock.exitCriteriaMet) ? phaseBlock.exitCriteriaMet : [],
44
- remaining: Array.isArray(phaseBlock.exitCriteriaRemaining) ? phaseBlock.exitCriteriaRemaining : [],
45
- };
46
- }
47
-
48
- const criteria = phaseBlock.exitCriteria;
49
- if (criteria && typeof criteria === "object" && !Array.isArray(criteria)) {
50
- const met = [];
51
- const remaining = [];
52
- for (const [key, value] of Object.entries(criteria)) {
53
- if (value) met.push(key);
54
- else remaining.push(key);
55
- }
56
- return { met, remaining };
57
- }
58
-
59
- return { met: [], remaining: [] };
60
- }
61
-
62
18
  function run(cmd, args, opts = {}) {
63
19
  return new Promise((resolve, reject) => {
64
20
  const p = spawn(cmd, args, { stdio: "inherit", cwd: repoRoot, shell: isWin, ...opts });
@@ -142,70 +98,16 @@ async function cmdRun() {
142
98
  }
143
99
 
144
100
  function renderProgress() {
145
- if (!existsSync(trackingPath)) {
146
- return {
147
- text: "Phase state: appledger/profiles/forgetrail.yaml.",
148
- md: null,
149
- };
150
- }
151
- const t = JSON.parse(readFileSync(trackingPath, "utf8"));
152
- const record = typeof t.record === "string" ? t.record.replaceAll("\\", "/") : "";
153
- if (t.status === "pointer" && record.includes("appledger")) {
154
- return {
155
- text: "workflow_tracking.json is a pointer to appledger/. Read appledger/profiles/forgetrail.yaml.",
156
- md: null,
157
- };
158
- }
159
- const phase = t.currentPhase ?? "?";
160
- const phaseId = String(phase);
161
- const phaseBlock = t.phases?.[phaseId];
162
- const label = PHASE_LABELS[phaseId] || phaseBlock?.name || `Phase ${phase}`;
163
- const status = phaseBlock?.status || "unknown";
164
- const { met, remaining } = getExitCriteriaStatus(phaseBlock);
165
- const lines = [
166
- `ForgeTrail phase: ${label} (${phaseId}) — ${status}`,
167
- "",
168
- met.length ? "Done (this phase):" : "",
169
- ...met.map((k) => ` ✓ ${k}`),
170
- met.length ? "" : "",
171
- remaining.length ? "Still open:" : "Exit criteria for this phase look complete (confirm before advancing):",
172
- ...remaining.map((k) => ` - ${k}`),
173
- ];
174
- if (t.sessions?.length) {
175
- const last = t.sessions[t.sessions.length - 1];
176
- lines.push("", `Last session: ${last.date} — ${last.summary || "(no summary)"}`);
177
- }
178
- const md = [
179
- "# ForgeTrail progress",
180
- "",
181
- "_Human-readable snapshot read from a legacy workflow_tracking.json. After migrate, the source of truth is `appledger/profiles/forgetrail.yaml`._",
182
- "",
183
- `**Current phase:** ${label} (\`${phaseId}\`) — **${status}**`,
184
- "",
185
- "## What's done (this phase)",
186
- "",
187
- ...(met.length ? met.map((k) => `- [x] ${k}`) : ["- _(none marked met yet)_"]),
188
- "",
189
- "## Still to do (this phase)",
190
- "",
191
- ...(remaining.length ? remaining.map((k) => `- [ ] ${k}`) : ["- _(none listed — verify before advancing)_"]),
192
- "",
193
- "## Refresh",
194
- "",
195
- "Double-click **status.bat** (Windows) or run **./status.sh** / **pnpm run forgetrail:status** anytime.",
196
- "",
197
- ].join("\n");
198
- return { text: lines.join("\n"), md };
101
+ if (!existsSync(trackingPath)) return 'Phase state: appledger/profiles/forgetrail.yaml.';
102
+ try {
103
+ const tracking = JSON.parse(readFileSync(trackingPath, 'utf8'));
104
+ if (tracking.status === 'pointer') return 'Project state: appledger/profiles/forgetrail.yaml.';
105
+ } catch {}
106
+ return 'Legacy tracking conflict. Preserve the file. Current migration is retired; use historical recovery in an isolated copy. Project state belongs in appledger/.';
199
107
  }
200
108
 
201
109
  function cmdStatus() {
202
- const { text, md } = renderProgress();
203
- console.log(text);
204
- if (md) {
205
- mkdirSync(join(repoRoot, "docs"), { recursive: true });
206
- writeFileSync(progressPath, md, "utf8");
207
- console.log(`\nWrote ${progressPath}`);
208
- }
110
+ console.log(renderProgress());
209
111
  }
210
112
 
211
113
  const cmd = process.argv[2];
package/docs/NPM.md CHANGED
@@ -10,12 +10,26 @@ Two public unscoped packages:
10
10
 
11
11
  | Package | This tree | On npm until you publish | From | What it is |
12
12
  | --- | --- | --- | --- | --- |
13
- | [`forgetrail`](https://www.npmjs.com/package/forgetrail) | 0.5.5 | 0.5.5 | repo root | CLI + Lite + templates. `pnpm dlx forgetrail install --lite` |
14
- | [`forgetrail-mcp`](https://www.npmjs.com/package/forgetrail-mcp) | 0.4.3 | 0.4.3 | `mcp-server/` | MCP bin. Depends on `forgetrail` for content. `pnpm dlx forgetrail-mcp` |
13
+ | [`forgetrail`](https://www.npmjs.com/package/forgetrail) | 0.5.8 | 0.5.7 | repo root | CLI + Lite + templates. `pnpm dlx forgetrail install --lite` |
14
+ | [`forgetrail-mcp`](https://www.npmjs.com/package/forgetrail-mcp) | 0.4.4 | 0.4.3 | `mcp-server/` | MCP bin. Depends on `forgetrail` for content. `pnpm dlx forgetrail-mcp` |
15
15
 
16
16
  These are **installer and MCP channels**, not a library. App repos should not add `forgetrail` to `dependencies`.
17
17
 
18
- `forgetrail@0.5.5` and `forgetrail-mcp@0.4.3` are on npm and match these `package.json` versions as of 2026-09-29. A later content change needs a version bump before it publishes. `getCompanionSuggestions` reads its text from the installed `forgetrail`. `pnpm run pack:check` is the gate. It does not publish.
18
+ The registry versions above were checked on 2026-09-30. The new tree versions are prepared and unpublished. `getCompanionSuggestions` reads its text from the installed `forgetrail`. `pnpm run pack:check` is the gate. It does not publish.
19
+
20
+ ## September ecosystem release
21
+
22
+ The CLI release removes writable legacy tracking templates and structural compatibility readers, preserves ledger pointer/conflict diagnosis, and clarifies when an agent creates AGENTS.md. AppLedger remains the application record; its completed one-time migration is retired separately.
23
+
24
+ Publish CLI `0.5.8` first. Before publishing MCP `0.4.4`, update its dependency and lockfile to require the new content:
25
+
26
+ ```powershell
27
+ Set-Location Z:\workspace\forgetrail\mcp-server
28
+ pnpm add "forgetrail@^0.5.8"
29
+ pnpm build
30
+ ```
31
+
32
+ Review and commit those files, then test the packaged MCP against that dependency. This update is pending until CLI `0.5.8` exists in the registry; the currently installed/locked dependency is still `0.5.2`. Do not publish the MCP preparation unchanged. Preserve independent package versions.
19
33
 
20
34
  ---
21
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgetrail",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "private": false,
5
5
  "description": "ForgeTrail: a persistent development system for building software with AI agents. CLI, Lite protocol, and methodology templates.",
6
6
  "license": "Apache-2.0",
@@ -15,7 +15,6 @@
15
15
  "prompts/",
16
16
  "WORKFLOW.md",
17
17
  "TRACKING_SCHEMA.md",
18
- "workflow_tracking.json",
19
18
  "INITIAL_PROMPT.md",
20
19
  "CONTINUATION_PROMPT.md",
21
20
  "TRY_FORGETRAIL.md",
@@ -28,7 +28,7 @@ test("a legacy tracking document is a conflict", () => {
28
28
  sessions: [],
29
29
  });
30
30
  assert.match(result.issues.join("\n"), /not the system of record/);
31
- assert.match(result.issues.join("\n"), /appledger migrate preview/);
31
+ assert.match(result.issues.join("\n"), /Legacy migration is retired/);
32
32
  });
33
33
 
34
34
  test("install does not create workflow_tracking.json", () => {
@@ -160,11 +160,11 @@ test("an edit under appledger/ runs check and reports a warning", () => {
160
160
 
161
161
  test("Lite and workflow instructions name the ledger", () => {
162
162
  const lite = readFileSync(join(root, "content", "FORGETRAIL_LITE.md"), "utf8");
163
- const legacyAt = lite.indexOf("## 11. Legacy");
163
+ const legacyAt = lite.indexOf("## 11. AppLedger record");
164
164
  const resumeAt = lite.indexOf("## 12. `AGENTS.md`");
165
165
  assert.ok(legacyAt > 0 && resumeAt > legacyAt);
166
166
  const instructional = `${lite.slice(0, legacyAt)}\n${lite.slice(resumeAt)}`;
167
- assert.match(lite.slice(legacyAt, resumeAt), /"schemaVersion": "lite-1"/);
167
+ assert.doesNotMatch(lite, /"schemaVersion": "lite-1"/);
168
168
  assert.match(instructional, /ForgeTrail Lite v2\.2\.2/);
169
169
  for (const line of instructional.split("\n")) {
170
170
  if (!line.includes("workflow_tracking.json")) continue;
@@ -55,33 +55,10 @@ function scanWorkspace(workspaceRoot) {
55
55
  if (trackingPath) {
56
56
  try {
57
57
  const tracking = JSON.parse(readFileSync(trackingPath, "utf-8"));
58
- projectName = tracking.project?.name || entry;
59
- phase = String(tracking.currentPhase || "-");
60
- archetype = tracking.project?.archetype || "product";
61
-
62
- if (Array.isArray(tracking.sessions) && tracking.sessions.length > 0) {
63
- const s = tracking.sessions[tracking.sessions.length - 1];
64
- const date = s.date || s.timestamp?.slice(0, 10) || "";
65
- const note = s.leftOff ? ` (${s.leftOff.slice(0, 30)}...)` : "";
66
- lastSession = date ? `${date}${note}` : "-";
67
- }
68
-
69
- const isLite = tracking.schemaVersion === "lite-1";
70
- if (isLite) {
71
- const pObj = tracking.phases?.[phase];
72
- if (pObj?.exitCriteria) {
73
- openCriteriaCount = Object.values(pObj.exitCriteria).filter((v) => !v).length;
74
- }
75
- } else {
76
- const pObj = tracking.phases?.[phase];
77
- if (Array.isArray(pObj?.exitCriteriaRemaining)) {
78
- openCriteriaCount = pObj.exitCriteriaRemaining.length;
79
- }
80
- }
81
58
  if (tracking.status === "pointer") {
82
59
  phase = ledger ? "appledger" : "pointer";
83
60
  } else if (tracking.currentPhase || tracking.phases) {
84
- phase = ledger ? `legacy+${phase}` : `legacy:${phase}`;
61
+ phase = "legacy conflict";
85
62
  }
86
63
  } catch {}
87
64
  }
@@ -1,125 +0,0 @@
1
- {
2
- "project": {
3
- "name": "",
4
- "created": "",
5
- "description": "",
6
- "archetype": "product",
7
- "status": "active"
8
- },
9
- "currentPhase": "1-architecture",
10
- "phases": {
11
- "1-architecture": {
12
- "status": "not_started",
13
- "startedAt": null,
14
- "completedAt": null,
15
- "exitCriteriaMet": [],
16
- "exitCriteriaRemaining": [
17
- "Tech choices confirmed",
18
- "Folder structure confirmed",
19
- "Data model shape confirmed",
20
- "First feature batch defined",
21
- "Claude's understanding verified by user",
22
- "PHASE_1_BRIEF.md created, complete, and locked (docs/)",
23
- "Major decisions recorded in workflow_tracking.json decisions[]"
24
- ],
25
- "notes": []
26
- },
27
- "2-scaffolding": {
28
- "status": "not_started",
29
- "startedAt": null,
30
- "completedAt": null,
31
- "exitCriteriaMet": [],
32
- "exitCriteriaRemaining": [
33
- "App runs with pnpm dev",
34
- "Real data visible on screen",
35
- "Core CRUD works",
36
- "Hero flow functions end to end",
37
- "CONTEXT_PROMPT.md created and populated by merging PHASE_1_BRIEF.md (then template lessons as needed)",
38
- "README.md created (from ForgeTrail template)",
39
- "TODO.md created (from ForgeTrail template, seeded from PHASE_1_BRIEF §11)",
40
- "IDEAS.md created (from ForgeTrail template)",
41
- "Runnable verify script in package.json (types + build or lint) passing cleanly",
42
- "Phase 2 doc scope only per WORKFLOW §1a (no premature extra templates)"
43
- ],
44
- "notes": []
45
- },
46
- "3-stabilization": {
47
- "status": "not_started",
48
- "startedAt": null,
49
- "completedAt": null,
50
- "exitCriteriaMet": [],
51
- "exitCriteriaRemaining": [
52
- "Environment variables resolve correctly",
53
- "APIs return real data",
54
- "Core workflow completes without infrastructure errors",
55
- "Error messages are human-readable (not just status codes)"
56
- ],
57
- "notes": []
58
- },
59
- "4-feature-iteration": {
60
- "status": "not_started",
61
- "startedAt": null,
62
- "completedAt": null,
63
- "exitCriteriaMet": [],
64
- "exitCriteriaRemaining": ["Core value proposition delivered", "Hero flow works with real data including edge cases", "Features tested beyond happy path"],
65
- "notes": [],
66
- "iterations": []
67
- },
68
- "5-refactoring": {
69
- "status": "not_started",
70
- "startedAt": null,
71
- "completedAt": null,
72
- "exitCriteriaMet": [],
73
- "exitCriteriaRemaining": [
74
- "No file exceeds ~500 lines without justification",
75
- "Shared utilities extracted",
76
- "No duplicated patterns",
77
- "Build passes after each refactor"
78
- ],
79
- "notes": []
80
- },
81
- "6-strategic-review": {
82
- "status": "not_started",
83
- "startedAt": null,
84
- "completedAt": null,
85
- "exitCriteriaMet": [],
86
- "exitCriteriaRemaining": [
87
- "BRAND_AND_PRODUCT.md created (from ForgeTrail template)",
88
- "TODO reorganized by brand value pillars",
89
- "Features mapped to brand pillars",
90
- "Gap between brand promise and product identified",
91
- "IDEAS.md processed into TODO",
92
- "Build order decided"
93
- ],
94
- "notes": []
95
- },
96
- "7-hardening": {
97
- "status": "not_started",
98
- "startedAt": null,
99
- "completedAt": null,
100
- "exitCriteriaMet": [],
101
- "exitCriteriaRemaining": [
102
- "CODE_QUALITY.md produced (from ForgeTrail template) and critical findings fixed",
103
- "BLACK_HAT_REPORT.md produced (via ForgeTrail security audit) and critical/high findings fixed",
104
- "All findings triaged into TODO.md with P0/P1/P2 priority",
105
- "All LLM outputs checked against field size limits",
106
- "No silent failures in core workflows (grep for empty catch blocks)",
107
- "All API routes return errors in consistent format",
108
- "All user input validated (centralized Zod schemas recommended)",
109
- "No unvalidated as-any casts on critical paths",
110
- "DEPLOYMENT.md created (from ForgeTrail template)",
111
- "BUSINESS_PLAN.md created if pursuing paid users (from ForgeTrail template)",
112
- "TEST_PLAN.md complete or extended (from ForgeTrail template)",
113
- "TECHNICAL_REFERENCE.md complete and current",
114
- "Auth flow works end to end (if applicable)",
115
- "Payment flow works end to end (if applicable)",
116
- "Docs alignment audit passed (via ForgeTrail docs-alignment audit)",
117
- "All documentation consolidated and cross-referenced"
118
- ],
119
- "notes": []
120
- }
121
- },
122
- "decisions": [],
123
- "gotchas": [],
124
- "sessions": []
125
- }