@jhizzard/termdeck 0.8.0 โ†’ 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/docs/orchestrator-guide.md +335 -0
  2. package/package.json +3 -1
  3. package/packages/cli/src/index.js +26 -3
  4. package/packages/cli/src/init-project.js +213 -0
  5. package/packages/cli/src/templates.js +84 -0
  6. package/packages/cli/templates/.claude-settings.json.tmpl +32 -0
  7. package/packages/cli/templates/.gitignore.tmpl +28 -0
  8. package/packages/cli/templates/CLAUDE.md.tmpl +35 -0
  9. package/packages/cli/templates/CONTRADICTIONS.md.tmpl +30 -0
  10. package/packages/cli/templates/README.md.tmpl +15 -0
  11. package/packages/cli/templates/RESTART-PROMPT.md.tmpl +38 -0
  12. package/packages/cli/templates/docs-orchestration-README.md.tmpl +29 -0
  13. package/packages/cli/templates/project_facts.md.tmpl +39 -0
  14. package/packages/client/public/app.js +781 -0
  15. package/packages/client/public/graph.html +104 -0
  16. package/packages/client/public/graph.js +683 -0
  17. package/packages/client/public/index.html +145 -0
  18. package/packages/client/public/style.css +1185 -0
  19. package/packages/server/src/graph-routes.js +555 -0
  20. package/packages/server/src/index.js +158 -5
  21. package/packages/server/src/orchestration-preview.js +256 -0
  22. package/packages/server/src/preflight.js +82 -0
  23. package/packages/server/src/rag.js +138 -0
  24. package/packages/server/src/setup/mnestra-migrations/009_memory_relationship_metadata.sql +126 -0
  25. package/packages/server/src/setup/mnestra-migrations/010_memory_recall_graph.sql +147 -0
  26. package/packages/server/src/setup/rumen/migrations/003_graph_inference_schedule.sql +49 -0
  27. package/packages/server/src/sprint-inject.js +156 -0
  28. package/packages/server/src/sprint-routes.js +503 -0
@@ -0,0 +1,335 @@
1
+ # Orchestrator Guide
2
+
3
+ A first-class reference for orchestrating Claude Code with TermDeck. This is the *how to work* doc โ€” not just *how to run terminals*. If you came here because TermDeck looked like a fancier tmux, you're in the right place. The terminals are the substrate; the orchestration patterns below are the product.
4
+
5
+ The patterns documented here were shaped over months of running AI coding sprints at Anthropic-model scale. Most of them were paid for in lost sleep, broken builds, and "why didn't anyone press Enter on panel 4 at 3 AM" debugging. They're conventions worth knowing before you have to discover them yourself.
6
+
7
+ This Guide is also rendered in the dashboard right-rail (top-right of the TermDeck UI). Press the `๐Ÿ“– Guide` tab to open it; it will auto-expand the section relevant to whatever you're focused on.
8
+
9
+ ---
10
+
11
+ ## 1. What is the 4+1 pattern?
12
+
13
+ The 4+1 pattern runs **four parallel Claude Code worker sessions (T1โ€“T4)** plus a **fifth orchestrator session** that coordinates them. Each worker owns a single lane of the sprint. The orchestrator never edits code in worker lanes; it briefs, monitors, audits, and closes.
14
+
15
+ ### Why parallel beats serial
16
+
17
+ Serial coding sessions hit two ceilings: (a) Claude's context fills up faster than the work shrinks, and (b) the human supervisor becomes the bottleneck because every step waits on review. Splitting work across four context-isolated workers gives you:
18
+
19
+ - **Cleaner context per lane.** Each worker only loads the files it owns. No bleed.
20
+ - **Honest audit trail.** Workers post FINDING / FIX-PROPOSED / DONE in `STATUS.md`; the orchestrator can't conflate "I think I'm done" with "the sprint is correct."
21
+ - **Parallel throughput.** Four lanes means four 30-minute jobs finish in 30 minutes, not two hours.
22
+ - **Recoverable failure.** A bad lane can be re-run without rolling back the others.
23
+
24
+ ### When to use 4+1 vs. a single session
25
+
26
+ Use 4+1 when:
27
+ - The work splits cleanly into 3โ€“4 file-owned lanes.
28
+ - You can write a one-page brief per lane in advance.
29
+ - You want a paper trail (`STATUS.md` becomes the sprint's diary).
30
+
31
+ Skip 4+1 (use a single Claude Code session) when:
32
+ - The work is exploratory ("figure out why X is slow").
33
+ - One lane gates all the others โ€” parallelism buys you nothing.
34
+ - The whole task is under ~30 minutes of work.
35
+
36
+ > **See also:** `~/.claude/CLAUDE.md` ยง MANDATORY: 4+1 sprint orchestration
37
+
38
+ ---
39
+
40
+ ## 2. The inject mandate
41
+
42
+ > **Cardinal rule: never copy-paste boot prompts. Always inject.**
43
+
44
+ When you launch a 4+1 sprint, the orchestrator pushes each worker's boot prompt into its panel via TermDeck's input API โ€” `POST /api/sessions/:id/input`. The human's job is to open four Claude Code terminals and say "terminals open, inject." The orchestrator does the rest.
45
+
46
+ Copy-pasting four boot prompts by hand is friction. It's also fragile: a stray newline, a missed paste, an out-of-order panel โ€” and the sprint starts wrong.
47
+
48
+ ### The two-stage submit pattern
49
+
50
+ This is the part that bit hard enough to become a hard rule. Each worker boot prompt is a multi-line bracketed-paste block. **Do not** append `\r` to the same POST. Use two POSTs:
51
+
52
+ 1. **Paste** โ€” `\x1b[200~<text>\x1b[201~` (no submit byte).
53
+ 2. **Settle** ~400 ms โ€” long enough for the PTY to flush the paste to Claude Code's input handler.
54
+ 3. **Submit** โ€” `\r` alone, in its own POST.
55
+
56
+ Why: when the close marker `\x1b[201~` and the trailing `\r` ride in one PTY write, the OS-level chunk boundary is non-deterministic. Sometimes Claude Code's input parser eats the `\r` as the last paste byte rather than a submit keystroke. Symptom: 3 of 4 panels auto-fire, the 4th sits at a visually populated input box waiting for a human to press Enter. **The cardinal sin is leaving a panel waiting for a human Enter press.** That cost real broken sleep on more than one overnight orchestration.
57
+
58
+ Single-stage `<text>\x1b[201~\r` injection is **banned**. Two-stage is the only sanctioned form.
59
+
60
+ ### Recovery when a panel stays idle
61
+
62
+ After all submits land, verify per-panel: `GET /api/sessions/:id/buffer` should show `status: 'thinking'` and a fresh `lastActivity`. If any panel is still `active` (idle) after ~8 s, the submit didn't land. Recover via `POST /api/sessions/:id/poke` with `methods: ['cr-flood']`. Don't page the human; this is exactly what `/poke` exists for.
63
+
64
+ ### cURL examples
65
+
66
+ Paste stage:
67
+
68
+ ```bash
69
+ curl -X POST http://127.0.0.1:3000/api/sessions/$SID/input \
70
+ -H 'Content-Type: application/json' \
71
+ --data-binary @- <<EOF
72
+ {"text": "[200~Hello, T1.\nBoot sequence:\n1. memory_recall(...)\n[201~", "source": "orchestrator"}
73
+ EOF
74
+ ```
75
+
76
+ Settle ~400 ms, then submit stage:
77
+
78
+ ```bash
79
+ curl -X POST http://127.0.0.1:3000/api/sessions/$SID/input \
80
+ -H 'Content-Type: application/json' \
81
+ -d '{"text":"\r","source":"orchestrator"}'
82
+ ```
83
+
84
+ Recovery (if a panel is stuck idle):
85
+
86
+ ```bash
87
+ curl -X POST http://127.0.0.1:3000/api/sessions/$SID/poke \
88
+ -H 'Content-Type: application/json' \
89
+ -d '{"methods":["cr-flood"]}'
90
+ ```
91
+
92
+ > **See also:** `~/.claude/CLAUDE.md` ยง MANDATORY: 4+1 sprint orchestration; `packages/server/src/index.js` (`/api/sessions/:id/input`, `/api/sessions/:id/poke`).
93
+
94
+ ---
95
+
96
+ ## 3. CLAUDE.md hierarchy
97
+
98
+ Three layers, read in order at session start. Each layer has a different lifespan and a different audience.
99
+
100
+ | Layer | File | Contains | Audience |
101
+ |---|---|---|---|
102
+ | **Global** | `~/.claude/CLAUDE.md` | Cross-project mandates: time check, memory-first, inject mandate, never-copy-paste, project directory map | Every Claude Code session, every project |
103
+ | **Project** | `./CLAUDE.md` (in the repo) | Read-order router. Hard rules specific to this project. Pointers to the canonical task docs | Every session in this repo |
104
+ | **Session** | The boot prompt itself (or `RESTART-PROMPT-YYYY-MM-DD.md`) | What this specific session must do, the topic to `memory_recall`, the active sprint plan to read | The single session being booted |
105
+
106
+ ### What goes in each layer
107
+
108
+ **Global rules** are things that apply *everywhere*. "Always check the time before saying 'tonight.'" "Always inject 4+1 boot prompts, never paste." These don't belong in a single repo because you'd duplicate them across every repo and they'd drift.
109
+
110
+ **Project router** is intentionally short. It says: "Here are this project's hard rules (no TypeScript, vanilla JS client, etc). For deeper context, here's the task-doc table โ€” read the *one* that matches your task." It does **not** restate global rules.
111
+
112
+ **Session prompt** carries the immediate intent. It tells the new session which `memory_recall` to fire first, which sprint plan to read, which lane it owns. It's disposable; the next session reads a new one.
113
+
114
+ ### Read-order matters
115
+
116
+ A worker session should always:
117
+
118
+ 1. `memory_recall(project=<this-project>, query=<task topic>)`
119
+ 2. Read `~/.claude/CLAUDE.md`
120
+ 3. Read `./CLAUDE.md`
121
+ 4. Read the one task doc the project router points to.
122
+ 5. Begin.
123
+
124
+ Skipping memory or skipping the project router leads to the same failure mode: the session "discovers" something the user already documented two sprints ago and proposes a fix that contradicts a locked decision. Don't do that.
125
+
126
+ > **See also:** `./CLAUDE.md` (this repo's router); `~/.claude/CLAUDE.md` ยง MANDATORY: Check Memory First.
127
+
128
+ ---
129
+
130
+ ## 4. Memory-first discipline
131
+
132
+ Claude Code has a persistent long-term memory system (the Mnestra MCP server, in the TMR stack). It survives sessions. It survives projects. It is the single most valuable input to any session that is not literally the first one.
133
+
134
+ ### Always start with `memory_recall`
135
+
136
+ Before reading files, before analyzing code, before writing anything: call `memory_recall`. Twice:
137
+
138
+ 1. With a query about the current project + task topic.
139
+ 2. With a broader query about recent decisions, bugs, or preferences.
140
+
141
+ The first surfaces lane-specific intel. The second surfaces drift you might otherwise ignore โ€” "oh, we decided last sprint that X is out of scope" is exactly the kind of thing memory catches.
142
+
143
+ ### When to use `memory_remember`
144
+
145
+ Save to memory when:
146
+
147
+ - You make a non-obvious architectural decision (lock it).
148
+ - You fix a bug whose root cause would surprise the next reader.
149
+ - You discover a user preference (workflow, tone, naming).
150
+ - You hit context near a soft compaction boundary and want a safety net.
151
+
152
+ Don't save to memory when:
153
+
154
+ - The fact lives perfectly well in code or `git log`. (Don't duplicate things `git blame` already answers.)
155
+ - The fact is ephemeral ("currently the build is red"). Memory is for things that should outlive the session.
156
+
157
+ ### Memory vs. project files
158
+
159
+ Memory persists across sessions and projects. Project files persist in the repo. Use:
160
+
161
+ - **Memory** for cross-session reasoning aids: decisions, preferences, surprising bug fixes.
162
+ - **`docs/`** for things you'd want a *new contributor* to read. README, ARCHITECTURE, RELEASE.
163
+ - **`CONTRADICTIONS.md`** for the live contradictions ledger โ€” facts that conflict and need resolution.
164
+ - **`project_facts.md`** for the *factual snapshot* of the project that doesn't change weekly.
165
+
166
+ ### Cross-project search
167
+
168
+ Omit the `project` parameter in `memory_recall` to search across all projects. Useful for shared patterns: "how did we handle Supabase migrations" โ€” answer might come from a sibling project.
169
+
170
+ > **See also:** `~/.claude/CLAUDE.md` ยง MANDATORY: Check Memory First; `~/.claude/CLAUDE.md` ยง RAG Memory System.
171
+
172
+ ---
173
+
174
+ ## 5. Enforcement vs. convention
175
+
176
+ When the orchestrator surfaces a security or correctness gap mid-sprint, the **default response is enforcement** โ€” fix the underlying mechanism so the gap can't recur. Convention-only ("we should remember to do X") is the fallback, not the default.
177
+
178
+ ### Why default-to-enforcement
179
+
180
+ A convention-only fix asks every future session to re-discover the rule, read the doc, and choose to follow it. That works for stylistic preferences. It does not work for security boundaries, data integrity, or correctness invariants โ€” there, the *first time someone forgets the convention* is the bug.
181
+
182
+ Enforcement looks like:
183
+
184
+ - Adding a runtime check that throws on misuse.
185
+ - Adding a CI lint that fails the PR.
186
+ - Restructuring the API so the wrong call is impossible to type.
187
+
188
+ ### When convention-only is justified
189
+
190
+ Convention is acceptable when **all three** of these hold:
191
+
192
+ 1. The cost of enforcement is disproportionate to the risk.
193
+ 2. The rule is genuinely contextual (case-by-case judgment, not a hard invariant).
194
+ 3. There's a clear paper trail (memory entry, CLAUDE.md note, doc) so future sessions encounter the rule.
195
+
196
+ If even one fails, prefer enforcement.
197
+
198
+ > **See also:** memory `feedback_orchestrator_enforcement.md`; `~/.claude/CLAUDE.md` ยง orchestration discipline.
199
+
200
+ ---
201
+
202
+ ## 6. Sprint discipline inside a lane
203
+
204
+ You are T*n*. You own *one* lane. Stay in it.
205
+
206
+ ### Hard rules for workers
207
+
208
+ - **No version bumps.** Don't edit `package.json`'s `version` field. Orchestrator handles it at close.
209
+ - **No `CHANGELOG.md` edits.** Same reason.
210
+ - **No commits.** Work, save files, sign DONE. The orchestrator commits the sprint as a single audited unit.
211
+ - **No `git push`, no `npm publish`.** Same reason โ€” and `RELEASE.md` has separate strict rules.
212
+ - **No edits outside your declared file ownership.** If you find a bug in another lane's files, post a FINDING in STATUS.md describing it; the orchestrator routes it.
213
+
214
+ ### STATUS.md โ€” append-only
215
+
216
+ Each lane has a `## T<n>` section in `docs/sprint-N-<name>/STATUS.md`. Post entries in this format:
217
+
218
+ ```
219
+ ### FINDING โ€” YYYY-MM-DD HH:MM ET
220
+ <what you found>
221
+
222
+ ### FIX-PROPOSED โ€” YYYY-MM-DD HH:MM ET
223
+ <what you intend to do>
224
+
225
+ ### DONE โ€” YYYY-MM-DD HH:MM ET
226
+ <files changed, line counts, anything follow-up sprints need to know>
227
+ ```
228
+
229
+ **Append only.** Never edit prior entries (yours or another lane's). The chronology is the audit trail.
230
+
231
+ ### What "DONE" means
232
+
233
+ DONE means: lane work is complete to the briefing's acceptance criteria, files are saved, a smoke test (manual or automated) confirms the change works. It does **not** mean "I've reviewed myself and decided I'm done." The orchestrator will independently verify before closing the sprint.
234
+
235
+ > **See also:** active sprint plan at `docs/sprint-N-<name>/PLANNING.md`; the lane briefing at `docs/sprint-N-<name>/T<n>-<lane>.md`.
236
+
237
+ ---
238
+
239
+ ## 7. Restart-prompt rituals
240
+
241
+ When a session ends and the next session needs to pick up cleanly, the bridge is a `RESTART-PROMPT-YYYY-MM-DD.md` written before close.
242
+
243
+ ### When to write one
244
+
245
+ Write a restart prompt when:
246
+
247
+ - A multi-day initiative paused mid-flight.
248
+ - The session is wrapping up but the work isn't done โ€” context window, not work, ran out.
249
+ - A sprint just shipped and the next sprint should start cold but informed.
250
+
251
+ If the session was a one-off Q&A with no shipped artifacts and no follow-up, skip the doc. (But still draft the session-end email per `~/.claude/CLAUDE.md` ยง Session-End Email โ€” that lives in Gmail, not the repo.)
252
+
253
+ ### What it must contain
254
+
255
+ A good restart prompt is paste-ready and self-contained. It has:
256
+
257
+ 1. **Live state** โ€” what's deployed, what's published, current branch, current versions.
258
+ 2. **What shipped this session** โ€” concrete bullets, dated, with commit SHAs.
259
+ 3. **What's planned next** โ€” queued sprints, their `docs/sprint-N-<name>/` paths, deferred items and why.
260
+ 4. **Read order for the next session** โ€” explicit list: `memory_recall(...)`, `~/.claude/CLAUDE.md`, `./CLAUDE.md`, the relevant restart doc, the active sprint plan.
261
+ 5. **Paste-ready prompt block** โ€” the literal text the human will hand to the next Claude session at boot.
262
+
263
+ Where to put it: top of the repo at `RESTART-PROMPT-YYYY-MM-DD.md`. If multiple per day, suffix with `-<topic>`.
264
+
265
+ > **See also:** `~/.claude/CLAUDE.md` ยง MANDATORY: Session-End Email to Self.
266
+
267
+ ---
268
+
269
+ ## 8. Per-project scaffolding files
270
+
271
+ A well-configured project repo has these files at root or near-root. They give every Claude session a consistent, low-friction onramp.
272
+
273
+ | File | Purpose |
274
+ |---|---|
275
+ | `CLAUDE.md` | The project router. Hard rules, read-order, pointers to task docs. Short. |
276
+ | `CONTRADICTIONS.md` | Live contradictions ledger. Facts that conflict and aren't yet resolved. New sessions read this to avoid relitigating settled debates. |
277
+ | `project_facts.md` | Factual snapshot โ€” what this project *is*, who built it, what's deployed where, what's published where. Updated at major milestones, not weekly. |
278
+ | `docs/orchestration/` | Sprint plans, restart prompts, sprint STATUS.md files. Each sprint gets a directory: `docs/sprint-N-<name>/PLANNING.md`, `T<n>-<lane>.md`, `STATUS.md`. |
279
+ | `RESTART-PROMPT.md` template | Skeleton for the restart-prompt ritual (ยง 7). Filled in at session end with live values. |
280
+ | `.claude/settings.json` | Permission defaults โ€” which commands to allow without prompting, hook configurations, etc. |
281
+
282
+ You don't need to create these by hand. The `termdeck init --project <name>` subcommand scaffolds all of them from canonical templates. The eight files generated, in their final project-relative target paths:
283
+
284
+ 1. `CLAUDE.md` โ€” project router (the one you're writing for).
285
+ 2. `CONTRADICTIONS.md` โ€” live contradictions ledger.
286
+ 3. `project_facts.md` โ€” factual snapshot, updated at milestones.
287
+ 4. `README.md` โ€” public-facing overview (extend after generation).
288
+ 5. `docs/orchestration/README.md` โ€” how this project runs sprints.
289
+ 6. `docs/orchestration/RESTART-PROMPT.md.tmpl` โ€” restart-prompt skeleton, copy + fill at session-end.
290
+ 7. `.claude/settings.json` โ€” permission allow/deny + hook defaults.
291
+ 8. `.gitignore` โ€” sensible defaults so secrets and per-machine state don't ship.
292
+
293
+ The `packages/cli/templates/` directory is the source of truth for the templates themselves; the Guide does not duplicate their content. To inspect or customize a template, read it there.
294
+
295
+ The dashboard's project drawer also surfaces an *orchestration preview* (Sprint 37 lane T3) that shows what `init --project` would generate for a given project, before you commit to the scaffolding.
296
+
297
+ > **See also:** `packages/cli/templates/` (template source of truth); `packages/cli/src/init-project.js` (the scaffolder); the project drawer's orchestration-preview tab.
298
+
299
+ ---
300
+
301
+ ## 9. Channel inject patterns
302
+
303
+ The inject mandate (ยง 2) is about Claude Code panels. The same principle applies to *messaging humans*: don't stop at "here's a draft, paste into iMessage." Deliver the message into the platform's compose box, ready to send.
304
+
305
+ ### WhatsApp
306
+
307
+ ```bash
308
+ URL="wa.me/<E164-without-plus>?text=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "Hello there.")"
309
+ open "https://$URL"
310
+ ```
311
+
312
+ WhatsApp Desktop intercepts the deep link and pre-fills the message in the conversation. Use `python3` quoting so special characters survive intact.
313
+
314
+ ### iMessage / SMS
315
+
316
+ Use the `mcp__imessage__send_imessage` MCP tool. `service: "iMessage"` is the default; pass `service: "SMS"` for Android contacts. The tool dispatches via AppleScript through Messages.app โ€” no copy-paste, no compose-box context switch.
317
+
318
+ ### Self
319
+
320
+ Pass `to: "self"` in `send_imessage` to route to the operator's own number (resolved via the `IMESSAGE_SELF_ADDRESS` env var). Useful for shipping a wrap-up summary, restart prompt, or "remind me about X" note to your own phone.
321
+
322
+ Contact lookup is the orchestrator's job, not the human's. If you don't know a phone number, search memory (`memory_recall` for `CONTACT <name>`), grep `~/.claude/cache/`, find it. Do not ask "what's their number?" if it has been provided in any prior session.
323
+
324
+ > **See also:** `~/.claude/CLAUDE.md` ยง MANDATORY: Never present messages for copy-paste โ€” always inject.
325
+
326
+ ---
327
+
328
+ ## Where to go next
329
+
330
+ - **Run a sprint right now:** open the dashboard, click the layout button labeled `orch`, launch four Claude Code panels + a fifth orchestrator panel, and tell the orchestrator "terminals open, inject."
331
+ - **Scaffold a new project:** `termdeck init --project <name>` โ€” see ยง 8.
332
+ - **Read the canonical sources:** `~/.claude/CLAUDE.md` for the global mandates this Guide is distilled from.
333
+ - **Find a specific section in the dashboard:** open the right-rail Guide panel (top-right) and search.
334
+
335
+ This Guide is reference material โ€” read the section that matches your situation, skip the rest. The next time you orchestrate a sprint, the patterns above should feel like muscle memory, not a doc to consult.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
5
5
  "bin": {
6
6
  "termdeck": "./packages/cli/src/index.js"
@@ -8,11 +8,13 @@
8
8
  "main": "packages/cli/src/index.js",
9
9
  "files": [
10
10
  "packages/cli/src/**",
11
+ "packages/cli/templates/**",
11
12
  "packages/server/src/**",
12
13
  "packages/client/public/**",
13
14
  "config/config.example.yaml",
14
15
  "config/secrets.env.example",
15
16
  "config/transcript-migration.sql",
17
+ "docs/orchestrator-guide.md",
16
18
  "LICENSE",
17
19
  "README.md"
18
20
  ],
@@ -111,6 +111,18 @@ const args = process.argv.slice(2);
111
111
  if (args[0] === 'init') {
112
112
  const mode = args[1];
113
113
  const rest = args.slice(2);
114
+
115
+ // Sprint 37 T2: refuse mode-mixing. The dispatch picks args[1] as the
116
+ // single mode flag, but a user who writes `init --project foo --mnestra`
117
+ // probably intended only one of those. Surface the conflict instead of
118
+ // silently picking the first.
119
+ const MODES = ['--project', '--mnestra', '--rumen'];
120
+ const presentModes = MODES.filter((m) => args.slice(1).includes(m));
121
+ if (presentModes.length > 1) {
122
+ console.error(`[cli] init: pass only one of ${MODES.join(' | ')}; got ${presentModes.join(' + ')}`);
123
+ process.exit(1);
124
+ }
125
+
114
126
  const run = (modPath) => {
115
127
  const fn = require(modPath);
116
128
  return fn(rest).then((code) => process.exit(code || 0));
@@ -129,9 +141,19 @@ if (args[0] === 'init') {
129
141
  });
130
142
  return;
131
143
  }
132
- console.error('Usage: termdeck init --mnestra | --rumen');
133
- console.error(' termdeck init --mnestra Configure Tier 2 memory (Supabase + Mnestra)');
134
- console.error(' termdeck init --rumen Deploy Tier 3 async learning (Rumen)');
144
+ if (mode === '--project') {
145
+ // init-project takes the project name as its first positional arg, plus
146
+ // optional --dry-run / --force flags. Pass `rest` straight through.
147
+ run(path.join(__dirname, 'init-project.js')).catch((err) => {
148
+ console.error('[cli] init --project failed:', err && err.stack || err);
149
+ process.exit(1);
150
+ });
151
+ return;
152
+ }
153
+ console.error('Usage: termdeck init --mnestra | --rumen | --project <name>');
154
+ console.error(' termdeck init --mnestra Configure Tier 2 memory (Supabase + Mnestra)');
155
+ console.error(' termdeck init --rumen Deploy Tier 3 async learning (Rumen)');
156
+ console.error(' termdeck init --project <name> Scaffold a new project with CLAUDE.md + orchestration docs');
135
157
  process.exit(1);
136
158
  }
137
159
 
@@ -214,6 +236,7 @@ for (let i = 0; i < args.length; i++) {
214
236
  termdeck --session-logs Write per-session markdown logs to ~/.termdeck/sessions/
215
237
  termdeck init --mnestra Configure Tier 2 memory (Supabase + Mnestra)
216
238
  termdeck init --rumen Deploy Tier 3 async learning (Rumen)
239
+ termdeck init --project NAME Scaffold a new project with CLAUDE.md + orchestration docs (--dry-run, --force)
217
240
  termdeck forge Generate Claude skills from memories (experimental)
218
241
  termdeck doctor Diagnose stack โ€” npm versions + Supabase schema (use --no-schema to skip the DB probe)
219
242
 
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ // `termdeck init --project <name>` โ€” Sprint 37 T2.
4
+ //
5
+ // Scaffolds a new project directory with the orchestration patterns TermDeck
6
+ // itself uses: CLAUDE.md (router), CONTRADICTIONS.md (audit trail),
7
+ // project_facts.md (stable facts), README.md, docs/orchestration/ (sprint +
8
+ // restart-prompt scaffolding), .claude/settings.json (sensible permission
9
+ // defaults), and .gitignore. All content comes from packages/cli/templates/
10
+ // rendered with {{placeholder}} substitution via packages/cli/src/templates.js.
11
+ //
12
+ // Public API (used by the CLI entry and by tests):
13
+ // initProject({ name, dryRun, force, cwd }) -> Promise<{ exitCode, files }>
14
+ //
15
+ // CLI shim (used by index.js dispatch):
16
+ // main(argv) -> Promise<exitCode>
17
+ //
18
+ // `argv` here is everything AFTER `init --project` in the original argv โ€”
19
+ // i.e. for `termdeck init --project hello --dry-run`, argv is
20
+ // `['hello', '--dry-run']`.
21
+
22
+ 'use strict';
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ const { listTemplates, renderTemplate, TEMPLATES_DIR } = require(path.join(__dirname, 'templates.js'));
28
+
29
+ // Project name validation: lowercase letters, digits, hyphens, optional
30
+ // scoped prefix (@org/name) is intentionally NOT supported here โ€” the user
31
+ // would clone the result and rename if they want a scoped npm package.
32
+ const NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
33
+
34
+ function validateName(name) {
35
+ if (typeof name !== 'string' || name.length === 0) {
36
+ return 'Project name is required.';
37
+ }
38
+ if (name.includes('/') || name.includes('\\') || name.includes('..')) {
39
+ return `Project name "${name}" must not contain slashes or "..".`;
40
+ }
41
+ if (!NAME_RE.test(name)) {
42
+ return `Project name "${name}" must be lowercase letters, digits, and hyphens (no leading/trailing hyphen).`;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function readTermdeckVersion() {
48
+ try {
49
+ const pkg = require(path.join(__dirname, '..', '..', '..', 'package.json'));
50
+ return pkg.version || '0.0.0';
51
+ } catch (_e) {
52
+ return '0.0.0';
53
+ }
54
+ }
55
+
56
+ function buildVars({ name, projectPath }) {
57
+ return {
58
+ project_name: name,
59
+ project_path: projectPath,
60
+ generated_at: new Date().toISOString(),
61
+ termdeck_version: readTermdeckVersion(),
62
+ };
63
+ }
64
+
65
+ function ensureDir(dir) {
66
+ fs.mkdirSync(dir, { recursive: true });
67
+ }
68
+
69
+ // Returns true if the directory either does not exist or exists and is empty.
70
+ function isEmptyOrMissing(dir) {
71
+ if (!fs.existsSync(dir)) return true;
72
+ const stat = fs.statSync(dir);
73
+ if (!stat.isDirectory()) return false;
74
+ return fs.readdirSync(dir).length === 0;
75
+ }
76
+
77
+ function previewSnippet(content, headLines = 5) {
78
+ const lines = content.split('\n');
79
+ const head = lines.slice(0, headLines).join('\n');
80
+ const remaining = Math.max(0, lines.length - headLines);
81
+ return remaining === 0 ? head : `${head}\n... (${remaining} more line${remaining === 1 ? '' : 's'})`;
82
+ }
83
+
84
+ async function initProject(opts) {
85
+ const { name, dryRun = false, force = false, cwd = process.cwd() } = opts || {};
86
+
87
+ const nameError = validateName(name);
88
+ if (nameError) {
89
+ process.stderr.write(`\n โœ— ${nameError}\n\n`);
90
+ return { exitCode: 1, files: [] };
91
+ }
92
+
93
+ const projectPath = path.resolve(cwd, name);
94
+
95
+ if (!dryRun && !force && !isEmptyOrMissing(projectPath)) {
96
+ process.stderr.write(`\n โœ— Target ${projectPath} exists and is not empty. Use --force to overwrite, or pick a new name.\n\n`);
97
+ return { exitCode: 1, files: [] };
98
+ }
99
+
100
+ const vars = buildVars({ name, projectPath });
101
+ const templates = listTemplates();
102
+ const written = [];
103
+
104
+ if (dryRun) {
105
+ process.stdout.write(`\n [dry-run] Would create ${projectPath}/ with ${templates.length} files:\n\n`);
106
+ } else {
107
+ ensureDir(projectPath);
108
+ }
109
+
110
+ for (const entry of templates) {
111
+ const dest = path.join(projectPath, entry.targetPath);
112
+ const rendered = renderTemplate(entry.name, vars);
113
+
114
+ if (dryRun) {
115
+ process.stdout.write(` โ€ข ${entry.targetPath}\n`);
116
+ const indented = previewSnippet(rendered).split('\n').map((l) => ` ${l}`).join('\n');
117
+ process.stdout.write(`${indented}\n\n`);
118
+ written.push({ template: entry.file, name: entry.name, dest, bytes: Buffer.byteLength(rendered, 'utf8') });
119
+ continue;
120
+ }
121
+
122
+ ensureDir(path.dirname(dest));
123
+ fs.writeFileSync(dest, rendered);
124
+ written.push({ template: entry.file, name: entry.name, dest, bytes: Buffer.byteLength(rendered, 'utf8') });
125
+ }
126
+
127
+ if (dryRun) {
128
+ process.stdout.write(` [dry-run] Nothing was written. Re-run without --dry-run to scaffold.\n\n`);
129
+ } else {
130
+ process.stdout.write(`
131
+ Created ${name}/ at ${projectPath}.
132
+
133
+ Next steps:
134
+ cd ${name}
135
+ git init
136
+ # Open ${name}/ in TermDeck โ€” it will pick up the .claude/settings.json automatically.
137
+ # Read CLAUDE.md to see the agent read-order for this project.
138
+
139
+ `);
140
+ }
141
+
142
+ return { exitCode: 0, files: written };
143
+ }
144
+
145
+ // CLI shim. Parses argv and calls initProject(). The dispatch in
146
+ // packages/cli/src/index.js strips the leading `init --project` tokens.
147
+ async function main(argv) {
148
+ const args = argv || [];
149
+
150
+ if (args.includes('--help') || args.includes('-h')) {
151
+ process.stdout.write(HELP);
152
+ return 0;
153
+ }
154
+
155
+ // First positional argument that isn't a flag is the project name.
156
+ let name = null;
157
+ let dryRun = false;
158
+ let force = false;
159
+
160
+ for (let i = 0; i < args.length; i++) {
161
+ const tok = args[i];
162
+ if (tok === '--dry-run') { dryRun = true; continue; }
163
+ if (tok === '--force') { force = true; continue; }
164
+ if (tok === '--name' && args[i + 1]) { name = args[i + 1]; i++; continue; }
165
+ if (tok.startsWith('--')) {
166
+ process.stderr.write(`\n โœ— Unknown flag: ${tok}\n${HELP}`);
167
+ return 1;
168
+ }
169
+ if (name === null) {
170
+ name = tok;
171
+ continue;
172
+ }
173
+ process.stderr.write(`\n โœ— Unexpected extra argument: ${tok}\n${HELP}`);
174
+ return 1;
175
+ }
176
+
177
+ if (!name) {
178
+ process.stderr.write(`\n โœ— Missing project name.\n${HELP}`);
179
+ return 1;
180
+ }
181
+
182
+ const { exitCode } = await initProject({ name, dryRun, force, cwd: process.cwd() });
183
+ return exitCode;
184
+ }
185
+
186
+ const HELP = `
187
+ TermDeck Project Scaffolder
188
+
189
+ Usage: termdeck init --project <name> [flags]
190
+
191
+ Flags:
192
+ --dry-run Print what would be created; write nothing
193
+ --force Overwrite an existing non-empty target directory
194
+ --help, -h Print this message and exit
195
+
196
+ What this does:
197
+ Creates <name>/ in the current directory with a project skeleton:
198
+ CLAUDE.md Agent read-order router
199
+ CONTRADICTIONS.md Audit trail of changed facts/decisions
200
+ project_facts.md Stable per-project facts
201
+ README.md Human-facing intro
202
+ docs/orchestration/ Sprint + restart-prompt scaffolding
203
+ .claude/settings.json Sensible Claude Code permission defaults
204
+ .gitignore Standard Node + .DS_Store + .termdeck/
205
+
206
+ Templates live in packages/cli/templates/ and use {{placeholder}} substitution.
207
+ `;
208
+
209
+ module.exports = main;
210
+ module.exports.initProject = initProject;
211
+ module.exports._validateName = validateName;
212
+ module.exports._buildVars = buildVars;
213
+ module.exports.TEMPLATES_DIR = TEMPLATES_DIR;