forgetrail 0.5.7 → 0.5.8
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/TRACKING_SCHEMA.md +2 -160
- package/content/FORGETRAIL_LITE.md +3 -49
- package/content/cursor-rules/forgetrail-phase-status.mdc +1 -1
- package/content/forgetrail-workspace-README.md +3 -3
- package/content/hooks/session-start.mjs +1 -1
- package/content/hooks/session-stop.mjs +1 -1
- package/content/hooks/validate-tracking-core.mjs +11 -253
- package/content/scripts/forgetrail-dev-launcher.mjs +8 -106
- package/docs/NPM.md +17 -3
- package/package.json +1 -2
- package/scripts/tracking-cutover.test.mjs +3 -3
- package/scripts/workspace-index.mjs +1 -24
- package/workflow_tracking.json +0 -125
package/TRACKING_SCHEMA.md
CHANGED
|
@@ -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`,
|
|
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
|
|
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
|
|
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.
|
|
948
|
+
## 11. AppLedger record
|
|
949
949
|
|
|
950
|
-
|
|
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:
|
|
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` |
|
|
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
|
-
|
|
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.
|
|
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("
|
|
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.
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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.
|
|
14
|
-
| [`forgetrail-mcp`](https://www.npmjs.com/package/forgetrail-mcp) | 0.4.
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.5.8",
|
|
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"), /
|
|
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.
|
|
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.
|
|
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 =
|
|
61
|
+
phase = "legacy conflict";
|
|
85
62
|
}
|
|
86
63
|
} catch {}
|
|
87
64
|
}
|
package/workflow_tracking.json
DELETED
|
@@ -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
|
-
}
|