@justethales/cockpit 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-05-30
4
+
5
+ Initial release.
6
+
7
+ - `cockpit init` — scaffolds a `cockpit/` directory with `state.json`, `now.md`, `roadmap.md`, `README.md`, and the three canonical templates (`session-prompt.md`, `session-log.md`, `audit-brief.md`).
8
+ - `cockpit check` — validator with 7 categories. Exits 1 on FAIL. ANSI output. `--quiet` flag for CI.
9
+ - `cockpit status` — one-screen snapshot. `--plain` flag strips ANSI.
10
+ - `cockpit new prompt --slug X` — copies the session-prompt template to `docs/plan/sessions/`.
11
+ - `cockpit new log --slug X` — copies the session-log template to `session-logs/`.
12
+ - Claude Code skills bundle (`skills/cockpit/`, `skills/next/`) — drop into `~/.claude/skills/` for instant `/cockpit` and `/next` slash commands.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thales (Juste Gnimavo) — ZeroSuite, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # Cockpit
2
+
3
+ > **A 200-line discipline for AI-coding sessions.** Machine-verifiable session state + drift validator + canonical templates. Works with Claude Code, Cursor, or anything that drives a repo with a session loop.
4
+
5
+ Built by [Thales (Juste Gnimavo)](https://thalesandhisaictoclaude.com) — solo CEO running six production products with one AI engineer. The cockpit is the operating system that keeps those sessions coherent across days, weeks, and feature gaps.
6
+
7
+ [Read the full story](https://thalesandhisaictoclaude.com) — the blog post that introduced this pattern explains the WHY. This README explains the HOW.
8
+
9
+ ---
10
+
11
+ ## Why this exists
12
+
13
+ You finish a Claude Code session at 11pm. You ship a feature. You think you closed the cockpit cleanly.
14
+
15
+ You open a fresh session at 9am. The agent reads your project's docs. It tells you "I see you shipped X yesterday — should I start Y?" You say yes.
16
+
17
+ Halfway through Y, the agent realizes Y was already shipped two days ago. The cockpit pointed at the wrong next thing. You just burned 90 minutes on a duplicate.
18
+
19
+ **That's drift.** It's the single most common failure mode of long-running AI coding workflows. Not bad code. Not security holes. Just the slow accumulation of "the cockpit says X but the code does Y" errors that eventually make the next session start with 30 minutes of re-orientation.
20
+
21
+ The cockpit fixes drift by :
22
+
23
+ 1. **Centralizing session state** in one `state.json` file (machine-readable).
24
+ 2. **Validating that state** against the filesystem and git on every push (`npx cockpit check`).
25
+ 3. **Surfacing the state** in one screen for any fresh session (`npx cockpit status`).
26
+ 4. **Templating the artifacts** every session produces (prompt + log + audit brief).
27
+
28
+ That's it. No tracking pixels, no SaaS, no required login. MIT license. 200 lines of TypeScript you can read in 10 minutes.
29
+
30
+ ---
31
+
32
+ ## Install
33
+
34
+ ### Quick start (no install)
35
+
36
+ ```bash
37
+ npx @thales/cockpit init # scaffold cockpit/ in current directory
38
+ npx @thales/cockpit status # one-screen snapshot
39
+ npx @thales/cockpit check # validate state.json against the world
40
+ ```
41
+
42
+ Works in any directory. Requires Node ≥ 20.
43
+
44
+ ### Global install (CLI everywhere)
45
+
46
+ ```bash
47
+ npm install -g @thales/cockpit
48
+ cockpit init
49
+ cockpit status
50
+ cockpit check
51
+ ```
52
+
53
+ ### Project install (committed to package.json)
54
+
55
+ ```bash
56
+ npm install --save-dev @thales/cockpit
57
+ ```
58
+
59
+ Add to `package.json` scripts :
60
+
61
+ ```json
62
+ {
63
+ "scripts": {
64
+ "cockpit:status": "cockpit status",
65
+ "cockpit:check": "cockpit check"
66
+ }
67
+ }
68
+ ```
69
+
70
+ Then `pnpm cockpit:status` / `pnpm cockpit:check` from any session.
71
+
72
+ ### Claude Code skills (slash commands)
73
+
74
+ ```bash
75
+ cp -r node_modules/@thales/cockpit/skills/cockpit ~/.claude/skills/
76
+ cp -r node_modules/@thales/cockpit/skills/next ~/.claude/skills/
77
+ ```
78
+
79
+ Now type `/cockpit` or `/next` in any Claude Code session. The skills wrap the CLI with the right pre-flight, fallback paths, and execution posture.
80
+
81
+ ---
82
+
83
+ ## Quickstart
84
+
85
+ ```bash
86
+ cd my-project
87
+ npx cockpit init
88
+ ```
89
+
90
+ That creates :
91
+
92
+ ```
93
+ cockpit/
94
+ ├── state.json # machine-readable session state
95
+ ├── now.md # what's the current focus (human-readable)
96
+ ├── roadmap.md # Next 3 to ship + phase scoreboard
97
+ ├── README.md # the protocol for your project
98
+ └── templates/
99
+ ├── session-prompt.md
100
+ ├── session-log.md
101
+ └── audit-brief.md
102
+ ```
103
+
104
+ Edit `cockpit/now.md` and `cockpit/roadmap.md` to describe your project. Edit `cockpit/state.json` to set the initial `current_phase` / `next_phase` / `next_prompt`.
105
+
106
+ Draft your first session prompt :
107
+
108
+ ```bash
109
+ npx cockpit new prompt --slug phase-1-first-slice
110
+ # edit docs/plan/sessions/PHASE-1-FIRST-SLICE.md
111
+ ```
112
+
113
+ At session start, the agent runs :
114
+
115
+ ```bash
116
+ npx cockpit status
117
+ ```
118
+
119
+ …which prints the snapshot and tells the agent which prompt file to open.
120
+
121
+ At session close, the agent runs (in order) :
122
+
123
+ ```bash
124
+ npx cockpit new log --slug what-shipped # write the log
125
+ # (edit the log, the prompt's frontmatter, now.md, roadmap.md, state.json)
126
+ npx cockpit check # 0 FAIL before push
127
+ git add . && git commit && git push
128
+ ```
129
+
130
+ That's the loop.
131
+
132
+ ---
133
+
134
+ ## What the validator catches
135
+
136
+ ```
137
+ $ npx cockpit check
138
+
139
+ cockpit:check · 22 PASS · 2 WARN · 1 FAIL
140
+ ──────────────────────────────────────────────────────────────────────
141
+ PASS state.json has 'next_prompt'
142
+ PASS next_prompt file exists · docs/plan/sessions/PHASE-1-AUTH.md
143
+ FAIL next_prompt is already SHIPPED · docs/plan/sessions/PHASE-1-AUTH.md has status: shipped — cockpit was not bumped after that session
144
+ → either (a) update state.json.next_prompt to the real next slice, or (b) re-execute the shipped prompt explicitly
145
+ ...
146
+ WARN last_commit is in history but not at HEAD · state=abc1234 HEAD=def5678
147
+ → bump state.last_commit to def5678
148
+ ...
149
+
150
+ ✗ 1 drift detected. Fix before push.
151
+ ```
152
+
153
+ Eight check categories. The full list is in [cockpit/README.md](templates/README.md) after init. The high-value ones :
154
+
155
+ - `state.json.next_prompt` points at a missing file or a `shipped` prompt.
156
+ - `state.json.last_session_id` doesn't map to a session-log file.
157
+ - `state.json.last_commit` not in `git log`.
158
+ - `state.json.phases_shipped[]` has duplicates.
159
+ - `state.json.migrations_applied[]` doesn't match the migrations directory.
160
+ - A session prompt has `status: shipped` but no `session_log:` pointer.
161
+ - Uncommitted changes in cockpit-area files.
162
+
163
+ Each failure prints a `→ fix` hint.
164
+
165
+ ---
166
+
167
+ ## Philosophy
168
+
169
+ Three opinions baked into the cockpit. Take them or fork it.
170
+
171
+ ### 1. **Templates are gates, not guidance.**
172
+
173
+ Every session produces three artifacts : the session prompt (drafted at the end of the *previous* session), the session log (written at the end of *this* session), and the audit brief (used to spawn the post-implementation auditor). All three have an implicit shape that prior projects had to discover by mirroring earlier files. The cockpit's templates make that shape explicit. When you draft a new prompt, run `cockpit new prompt --slug X` — don't copy-paste a prior one.
174
+
175
+ ### 2. **Validate state, not intent.**
176
+
177
+ The validator checks metadata : "does `state.json.next_prompt` point at a queued prompt, and does that prompt's frontmatter say `queued`?". It cannot check intent — whether your `now.md` paragraph accurately describes what was built. That's a humans-and-honest-self-review job. The post-implementation audit catches code-vs-spec drift ; nothing catches description-vs-reality drift. Acknowledge this and don't try to over-engineer it.
178
+
179
+ ### 3. **`cockpit check` is mandatory before push, no exceptions.**
180
+
181
+ The 200 ms cost of the validator is negligible. The cost of drift surfacing at session-start the next day is real (10–30 minutes of confusion). Make the validator a `pre-push` gate (manually, via a hook, or via your team's discipline). The day you skip it is the day you ship a state.json that lies about what was done.
182
+
183
+ ---
184
+
185
+ ## What this is NOT
186
+
187
+ - **Not a task tracker.** Use Linear, Jira, GitHub Issues. The cockpit is about *which session ships next*, not *which issues are open*.
188
+ - **Not a project management tool.** No timelines, no story points, no burndown charts.
189
+ - **Not a CI tool.** It runs locally. You can wire it into CI as a pre-push check, but it doesn't replace your test runner.
190
+ - **Not opinionated about your code.** It cares about session state. Your code can be Rust, Python, TypeScript, Go, anything.
191
+
192
+ ---
193
+
194
+ ## Project structure conventions
195
+
196
+ The cockpit assumes :
197
+
198
+ ```
199
+ your-project/
200
+ ├── cockpit/ # the cockpit
201
+ │ ├── state.json
202
+ │ ├── now.md
203
+ │ ├── roadmap.md
204
+ │ ├── README.md
205
+ │ └── templates/
206
+ ├── docs/plan/sessions/ # session prompts
207
+ │ ├── PHASE-1-AUTH.md
208
+ │ ├── PHASE-2-PROFILE.md
209
+ │ └── ...
210
+ ├── session-logs/ # session logs
211
+ │ ├── 26-05-30-001-phase-1-auth.md
212
+ │ ├── 26-05-30-002-phase-2-profile.md
213
+ │ └── ...
214
+ └── (your code)
215
+ ```
216
+
217
+ If you use different paths, you can adjust them in `cockpit/state.json`'s `migrations_dir` field (for migration paths) and edit the templates. The validator paths are currently hardcoded ; configurability is a v0.2 thing if there's demand.
218
+
219
+ ---
220
+
221
+ ## FAQ
222
+
223
+ **Q : Does this require Claude Code?**
224
+ No. The CLI works standalone. The skills bundle (`skills/cockpit`, `skills/next`) is opt-in for Claude Code users who want the slash commands.
225
+
226
+ **Q : Does this work for Cursor / Aider / Continue?**
227
+ Yes. The CLI is just a CLI. Any agent that can run shell commands and read files can use it. The slash-command skills are Claude Code-specific, but the underlying discipline transfers.
228
+
229
+ **Q : What about Python / Rust / Go projects?**
230
+ The CLI is Node-only (TypeScript via `tsx`). You can install via `npx` without committing Node as a project dependency. If you want a binary distribution, file an issue — it's a 1-day port.
231
+
232
+ **Q : Can I customize the templates?**
233
+ Yes. After `cockpit init`, the templates live in your project at `cockpit/templates/`. Edit them freely. The CLI's `cockpit new` commands copy from your project's templates if they exist.
234
+
235
+ **Q : What if my project doesn't have phases?**
236
+ The cockpit uses "phase" as a generic word for "unit of work that ships together." You can map it to "epic," "milestone," "sprint," or "release" — same machinery.
237
+
238
+ **Q : Is the cockpit data sent anywhere?**
239
+ No. Zero network calls. Zero telemetry. The cockpit reads your filesystem and your `git`. Nothing leaves your machine.
240
+
241
+ **Q : Where do I report bugs / feature requests?**
242
+ https://github.com/justethales/cockpit-skill/issues
243
+
244
+ ---
245
+
246
+ ## License
247
+
248
+ MIT. Use it, fork it, ship it.
249
+
250
+ If the cockpit saves you 30 minutes a week, that's enough payback. If you want to say thanks : star the GitHub repo, share the blog post, or [say hi on X](https://x.com/JusteThales).
251
+
252
+ ---
253
+
254
+ *Cockpit is a small thing. Most of the value is in the discipline it forces, not in the 200 lines of TypeScript. Read the [blog post](https://thalesandhisaictoclaude.com) for the full story behind the pattern.*
package/dist/check.js ADDED
@@ -0,0 +1,228 @@
1
+ /**
2
+ * `cockpit check` — validate state.json against filesystem + git + prompt frontmatter.
3
+ *
4
+ * Exit code 1 on any FAIL. Use --quiet to suppress PASS lines (CI-friendly).
5
+ */
6
+ import { existsSync, readdirSync, statSync } from 'node:fs';
7
+ import { join, relative } from 'node:path';
8
+ import { exit } from 'node:process';
9
+ import { c, git, loadState, readFrontmatter } from './shared.js';
10
+ const ROOT = process.cwd();
11
+ const STATE_PATH = join(ROOT, 'cockpit', 'state.json');
12
+ const SESSIONS_DIR = join(ROOT, 'docs', 'plan', 'sessions');
13
+ const LOGS_DIR = join(ROOT, 'session-logs');
14
+ export function runCheck(args) {
15
+ const quiet = args.includes('--quiet');
16
+ const noGit = args.includes('--no-git');
17
+ const findings = [];
18
+ function record(id, severity, label, detail, fix) {
19
+ findings.push({ id, severity, label, detail, fix });
20
+ }
21
+ if (!existsSync(STATE_PATH)) {
22
+ console.error(c.red('FAIL') + ` no cockpit/state.json found at ${STATE_PATH}`);
23
+ console.error(c.gray(' → run `npx cockpit init` first'));
24
+ exit(1);
25
+ }
26
+ const state = loadState(STATE_PATH);
27
+ if (!state) {
28
+ console.error(c.red('FAIL') + ' cockpit/state.json is not valid JSON');
29
+ exit(1);
30
+ }
31
+ /* 1. Required keys ----------------------------------------------------- */
32
+ const required = [
33
+ 'updated_at',
34
+ 'last_session_id',
35
+ 'last_commit',
36
+ 'current_phase',
37
+ 'next_phase',
38
+ 'next_prompt',
39
+ 'phases_shipped'
40
+ ];
41
+ for (const key of required) {
42
+ if (state[key] === undefined || state[key] === null) {
43
+ record(`state.shape.${key}`, 'fail', 'state.json missing required key', `key '${key}' is missing`, `add "${key}": <value> to cockpit/state.json`);
44
+ }
45
+ else {
46
+ record(`state.shape.${key}`, 'pass', `state.json has '${key}'`, '');
47
+ }
48
+ }
49
+ /* 2. next_prompt resolves --------------------------------------------- */
50
+ if (state.next_prompt) {
51
+ const path = join(ROOT, state.next_prompt);
52
+ if (!existsSync(path)) {
53
+ record('next_prompt.exists', 'fail', 'state.json.next_prompt points at a missing file', `${state.next_prompt} does not exist`, `draft the prompt at that path (try \`npx cockpit new prompt --slug <slug>\`) OR fix state.json.next_prompt`);
54
+ }
55
+ else {
56
+ record('next_prompt.exists', 'pass', 'next_prompt file exists', state.next_prompt);
57
+ const fm = readFrontmatter(path);
58
+ if (!fm) {
59
+ record('next_prompt.frontmatter', 'fail', 'next_prompt has no frontmatter', `${state.next_prompt} should start with --- ... ---`, `add status / session_id / drafted_at frontmatter`);
60
+ }
61
+ else {
62
+ const status = String(fm.status ?? '');
63
+ if (status === 'shipped') {
64
+ record('next_prompt.status', 'fail', 'next_prompt is already SHIPPED', `${state.next_prompt} has status: shipped — cockpit was not bumped after that session`, `either update state.json.next_prompt to the real next slice, or re-execute the shipped prompt explicitly`);
65
+ }
66
+ else if (status === 'queued' || status === 'in-progress') {
67
+ record('next_prompt.status', 'pass', `next_prompt status is '${status}'`, state.next_prompt);
68
+ }
69
+ else {
70
+ record('next_prompt.status', 'warn', 'next_prompt has unusual status', `status='${status}' — expected 'queued' or 'in-progress'`, `set status: queued in the prompt frontmatter`);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ /* 3. last_session_id → log -------------------------------------------- */
76
+ if (state.last_session_id) {
77
+ const logPath = join(LOGS_DIR, `${state.last_session_id}.md`);
78
+ if (existsSync(logPath)) {
79
+ record('last_session.log_exists', 'pass', 'last_session_id has a matching session log', `session-logs/${state.last_session_id}.md`);
80
+ }
81
+ else {
82
+ record('last_session.log_exists', 'fail', 'last_session_id does not map to a session log', `expected session-logs/${state.last_session_id}.md`, `write the session log (try \`npx cockpit new log --slug <slug>\`) OR fix last_session_id`);
83
+ }
84
+ }
85
+ /* 4. last_commit vs git ----------------------------------------------- */
86
+ if (!noGit && state.last_commit && state.last_commit !== 'pending') {
87
+ const head = git('rev-parse --short HEAD');
88
+ if (!head) {
89
+ record('last_commit.git', 'warn', 'cannot run git', 'is this a repo?');
90
+ }
91
+ else if (head.startsWith(state.last_commit) ||
92
+ state.last_commit.startsWith(head)) {
93
+ record('last_commit.git', 'pass', 'last_commit matches HEAD', `state=${state.last_commit} HEAD=${head}`);
94
+ }
95
+ else {
96
+ const exists = git(`rev-parse --verify ${state.last_commit}^{commit}`);
97
+ if (exists) {
98
+ record('last_commit.git', 'warn', 'last_commit is in history but not at HEAD', `state=${state.last_commit} HEAD=${head}`, `if the new commits are uncockpitted work, bump state.last_commit to ${head}`);
99
+ }
100
+ else {
101
+ record('last_commit.git', 'fail', 'last_commit not found in git', `state=${state.last_commit} does not exist in this repo`, `set state.last_commit to a real SHA (HEAD = ${head})`);
102
+ }
103
+ }
104
+ }
105
+ else if (state.last_commit === 'pending') {
106
+ record('last_commit.git', 'warn', "last_commit is 'pending'", 'expected after first commit of session, before push', 'bump state.last_commit to the commit SHA before push');
107
+ }
108
+ /* 5. phases_shipped uniqueness ---------------------------------------- */
109
+ if (Array.isArray(state.phases_shipped)) {
110
+ const seen = new Set();
111
+ const dupes = [];
112
+ for (const p of state.phases_shipped) {
113
+ if (seen.has(p))
114
+ dupes.push(p);
115
+ seen.add(p);
116
+ }
117
+ if (dupes.length) {
118
+ record('phases_shipped.unique', 'fail', 'phases_shipped has duplicates', dupes.join(', '), 'dedupe the array');
119
+ }
120
+ else {
121
+ record('phases_shipped.unique', 'pass', `phases_shipped is unique (${state.phases_shipped.length} entries)`, '');
122
+ }
123
+ }
124
+ /* 6. migrations_applied (optional) ------------------------------------- */
125
+ const migrationsDir = join(ROOT, state.migrations_dir || 'drizzle');
126
+ if (Array.isArray(state.migrations_applied) && existsSync(migrationsDir)) {
127
+ const onDisk = readdirSync(migrationsDir)
128
+ .filter((f) => f.endsWith('.sql'))
129
+ .map((f) => f.replace(/\.sql$/, ''))
130
+ .sort();
131
+ const inState = [...state.migrations_applied].sort();
132
+ const missingFromState = onDisk.filter((m) => !inState.includes(m));
133
+ const missingFromDisk = inState.filter((m) => !onDisk.includes(m));
134
+ if (missingFromState.length || missingFromDisk.length) {
135
+ record('migrations.match', 'fail', `migrations_applied does not match ${state.migrations_dir || 'drizzle'}/`, `state-missing: ${missingFromState.join(', ') || '(none)'} · disk-missing: ${missingFromDisk.join(', ') || '(none)'}`, 'add missing-from-state to state.migrations_applied OR remove ghosts');
136
+ }
137
+ else if (onDisk.length > 0) {
138
+ record('migrations.match', 'pass', `migrations_applied matches ${state.migrations_dir || 'drizzle'}/ (${onDisk.length} files)`, '');
139
+ }
140
+ }
141
+ /* 7. Session prompt frontmatter --------------------------------------- */
142
+ const VALID_STATUS = new Set(['queued', 'in-progress', 'shipped', 'archived']);
143
+ if (existsSync(SESSIONS_DIR)) {
144
+ const prompts = readdirSync(SESSIONS_DIR)
145
+ .filter((f) => f.endsWith('.md'))
146
+ .map((f) => join(SESSIONS_DIR, f))
147
+ .filter((f) => statSync(f).isFile());
148
+ let shippedWithoutLog = 0;
149
+ let invalidStatusValues = [];
150
+ for (const p of prompts) {
151
+ const rel = relative(ROOT, p);
152
+ const fm = readFrontmatter(p);
153
+ if (!fm) {
154
+ record(`prompt.${rel}.frontmatter`, 'warn', 'prompt has no parsable frontmatter', rel);
155
+ continue;
156
+ }
157
+ const status = String(fm.status ?? '');
158
+ if (!VALID_STATUS.has(status)) {
159
+ invalidStatusValues.push(`${rel} status='${status}'`);
160
+ }
161
+ if (status === 'shipped') {
162
+ const logField = String(fm.session_log ?? '');
163
+ if (!logField || logField === 'pending') {
164
+ shippedWithoutLog++;
165
+ record(`prompt.${rel}.session_log`, 'fail', 'shipped prompt has no session_log pointer', rel, 'set session_log: session-logs/<id>.md in the frontmatter');
166
+ }
167
+ else if (!existsSync(join(ROOT, logField))) {
168
+ record(`prompt.${rel}.session_log_exists`, 'fail', "shipped prompt's session_log file is missing", `${rel} -> ${logField}`, 'either write the missing log OR fix the pointer');
169
+ }
170
+ }
171
+ }
172
+ if (invalidStatusValues.length) {
173
+ record('prompts.status_values', 'warn', 'some prompts have non-canonical status values', invalidStatusValues.join(' · '), `use one of: ${[...VALID_STATUS].join(' | ')}`);
174
+ }
175
+ else if (prompts.length) {
176
+ record('prompts.status_values', 'pass', `all ${prompts.length} prompt(s) have canonical status`, '');
177
+ }
178
+ if (shippedWithoutLog === 0 && prompts.length) {
179
+ record('prompts.shipped_logged', 'pass', 'every shipped prompt has a session_log pointer', '');
180
+ }
181
+ }
182
+ /* 8. Working tree clean for cockpit + sessions + logs ----------------- */
183
+ if (!noGit) {
184
+ const dirty = git('status --porcelain cockpit docs/plan/sessions session-logs');
185
+ if (dirty) {
186
+ record('workdir.clean', 'warn', 'cockpit / sessions / logs have uncommitted changes', dirty.split('\n').slice(0, 5).join(' · '), 'commit + push before the session closes');
187
+ }
188
+ else {
189
+ record('workdir.clean', 'pass', 'cockpit + sessions + logs are committed', '');
190
+ }
191
+ }
192
+ /* Report --------------------------------------------------------------- */
193
+ const pass = findings.filter((f) => f.severity === 'pass').length;
194
+ const warn = findings.filter((f) => f.severity === 'warn').length;
195
+ const fail = findings.filter((f) => f.severity === 'fail').length;
196
+ if (!quiet || fail > 0) {
197
+ const head = `${c.bold('cockpit:check')} · ${pass} PASS · ${warn > 0 ? c.yellow(`${warn} WARN`) : `${warn} WARN`} · ${fail > 0 ? c.red(`${fail} FAIL`) : `${fail} FAIL`}`;
198
+ console.log('');
199
+ console.log(head);
200
+ console.log(c.gray('─'.repeat(70)));
201
+ for (const f of findings) {
202
+ const tag = f.severity === 'pass'
203
+ ? c.green('PASS')
204
+ : f.severity === 'warn'
205
+ ? c.yellow('WARN')
206
+ : c.red('FAIL');
207
+ if (f.severity === 'pass' && quiet)
208
+ continue;
209
+ const detail = f.detail ? c.gray(` · ${f.detail}`) : '';
210
+ console.log(` ${tag} ${f.label}${detail}`);
211
+ if (f.fix && f.severity !== 'pass') {
212
+ console.log(` ${c.cyan('→')} ${c.gray(f.fix)}`);
213
+ }
214
+ }
215
+ console.log('');
216
+ if (fail > 0) {
217
+ console.log(c.red(`✗ ${fail} drift${fail > 1 ? 's' : ''} detected. Fix before push.`));
218
+ }
219
+ else if (warn > 0) {
220
+ console.log(c.yellow(`⚠ ${warn} warning${warn > 1 ? 's' : ''} (not blocking).`));
221
+ }
222
+ else {
223
+ console.log(c.green('✓ cockpit is coherent. Push when ready.'));
224
+ }
225
+ console.log('');
226
+ }
227
+ exit(fail > 0 ? 1 : 0);
228
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cockpit — a 200-line discipline for AI-coding sessions.
4
+ *
5
+ * https://github.com/justethales/cockpit-skill
6
+ * MIT License.
7
+ */
8
+ import { argv, exit } from 'node:process';
9
+ import { runInit } from './init.js';
10
+ import { runCheck } from './check.js';
11
+ import { runStatus } from './status.js';
12
+ import { runNew } from './new.js';
13
+ const VERSION = '0.1.0';
14
+ const HELP = `
15
+ cockpit ${VERSION} — a 200-line discipline for AI-coding sessions
16
+
17
+ USAGE
18
+ cockpit <command> [options]
19
+
20
+ COMMANDS
21
+ init Scaffold cockpit/ in the current directory
22
+ status Print one-screen snapshot (use --plain for no color)
23
+ check Validate state.json against filesystem + git
24
+ check --quiet Same, suppress output unless FAIL
25
+ new prompt --slug <kebab-id> Copy session-prompt template to docs/plan/sessions/
26
+ new log --slug <kebab-id> Copy session-log template to session-logs/
27
+
28
+ GLOBAL
29
+ -h, --help Print this help
30
+ -V, --version Print version
31
+
32
+ EXAMPLES
33
+ cockpit init # in a fresh repo
34
+ cockpit status # at session start
35
+ cockpit check # before git push
36
+ cockpit new prompt --slug phase-2-auth-flow
37
+
38
+ LEARN MORE
39
+ https://github.com/justethales/cockpit-skill
40
+ https://thalesandhisaictoclaude.com
41
+ `;
42
+ function main() {
43
+ const args = argv.slice(2);
44
+ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
45
+ console.log(HELP);
46
+ exit(0);
47
+ }
48
+ if (args.includes('-V') || args.includes('--version')) {
49
+ console.log(VERSION);
50
+ exit(0);
51
+ }
52
+ const [cmd, ...rest] = args;
53
+ switch (cmd) {
54
+ case 'init':
55
+ runInit(rest);
56
+ break;
57
+ case 'status':
58
+ runStatus(rest);
59
+ break;
60
+ case 'check':
61
+ runCheck(rest);
62
+ break;
63
+ case 'new':
64
+ runNew(rest);
65
+ break;
66
+ default:
67
+ console.error(`unknown command: ${cmd}\n`);
68
+ console.log(HELP);
69
+ exit(1);
70
+ }
71
+ }
72
+ main();
package/dist/init.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `cockpit init` — scaffold a cockpit/ directory in the current repo.
3
+ */
4
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
5
+ import { dirname, join, relative } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { exit } from 'node:process';
8
+ import { c, todayISO } from './shared.js';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const TEMPLATES = join(__dirname, '..', 'templates');
12
+ function copyDir(src, dest, force) {
13
+ if (!existsSync(dest))
14
+ mkdirSync(dest, { recursive: true });
15
+ for (const entry of readdirSync(src)) {
16
+ const s = join(src, entry);
17
+ const d = join(dest, entry);
18
+ if (statSync(s).isDirectory()) {
19
+ copyDir(s, d, force);
20
+ }
21
+ else {
22
+ if (existsSync(d) && !force) {
23
+ console.log(` ${c.gray('skip')} ${relative(process.cwd(), d)} (exists)`);
24
+ continue;
25
+ }
26
+ copyFileSync(s, d);
27
+ console.log(` ${c.green('write')} ${relative(process.cwd(), d)}`);
28
+ }
29
+ }
30
+ }
31
+ function interpolate(filePath, vars) {
32
+ const raw = readFileSync(filePath, 'utf8');
33
+ let out = raw;
34
+ for (const [k, v] of Object.entries(vars)) {
35
+ out = out.split(`{{${k}}}`).join(v);
36
+ }
37
+ if (out !== raw)
38
+ writeFileSync(filePath, out);
39
+ }
40
+ export function runInit(args) {
41
+ const force = args.includes('--force') || args.includes('-f');
42
+ const root = process.cwd();
43
+ const target = join(root, 'cockpit');
44
+ if (existsSync(target) && !force) {
45
+ console.log(c.yellow(`cockpit/ already exists at ${target}`));
46
+ console.log(c.gray('use --force to overwrite, or edit the existing files'));
47
+ exit(0);
48
+ }
49
+ console.log(`${c.bold('cockpit init')} · scaffolding ${c.cyan(relative(root, target) || 'cockpit/')}`);
50
+ console.log('');
51
+ copyDir(TEMPLATES, target, force);
52
+ // Interpolate the date placeholder in scaffolded markdown.
53
+ const today = todayISO();
54
+ const interpolatables = [
55
+ join(target, 'now.md'),
56
+ join(target, 'roadmap.md'),
57
+ join(target, 'state.json'),
58
+ join(target, 'README.md')
59
+ ];
60
+ for (const f of interpolatables) {
61
+ if (existsSync(f))
62
+ interpolate(f, { TODAY: today });
63
+ }
64
+ console.log('');
65
+ console.log(c.green('✓ cockpit scaffolded.'));
66
+ console.log('');
67
+ console.log('Next steps :');
68
+ console.log(` ${c.cyan('1.')} edit ${c.gray('cockpit/now.md')} — describe your current focus`);
69
+ console.log(` ${c.cyan('2.')} edit ${c.gray('cockpit/roadmap.md')} — fill the Next 3 to ship`);
70
+ console.log(` ${c.cyan('3.')} edit ${c.gray('cockpit/state.json')} — fill current_phase / next_phase / next_prompt`);
71
+ console.log(` ${c.cyan('4.')} draft your first session prompt :`);
72
+ console.log(` ${c.gray('npx cockpit new prompt --slug my-first-session')}`);
73
+ console.log(` ${c.cyan('5.')} validate before push :`);
74
+ console.log(` ${c.gray('npx cockpit check')}`);
75
+ console.log('');
76
+ console.log(c.gray('full protocol → cockpit/README.md'));
77
+ console.log('');
78
+ }