@vibecompass/vibecompass 0.1.0 → 0.2.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/README.md +69 -14
- package/package.json +1 -1
- package/src/cli.js +139 -4
- package/src/generators/agent-files/template.js +11 -2
- package/src/index.js +6 -1
- package/src/scaffold.js +71 -31
- package/src/session.js +357 -43
- package/src/workflow.js +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ Shipped today:
|
|
|
14
14
|
- interactive guided init for placement, workflow bootstrap, and first-session setup
|
|
15
15
|
- generated `context.md` plus opt-in workflow guide files
|
|
16
16
|
- opt-in starter `CLAUDE.md` / `AGENTS.md` templates that are created once and never overwritten
|
|
17
|
-
- `vibecompass start-session` / `vibecompass close-session` for
|
|
17
|
+
- `vibecompass start-session` / `vibecompass close-session` for lane-aware local builder workflow
|
|
18
|
+
- `vibecompass list-sessions` / `vibecompass switch-session` for multiple active session lanes
|
|
19
|
+
- `vibecompass end-session` as a discoverable alias for `close-session`
|
|
18
20
|
- `vibecompass sync-agents` for generated `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, and `.github/copilot-instructions.md` views
|
|
19
21
|
- canonical file scanning and validation
|
|
20
22
|
- `state/manifest.json` generation
|
|
@@ -26,6 +28,8 @@ Not shipped yet:
|
|
|
26
28
|
|
|
27
29
|
The package is publishable now as the file-contract and read-model core. The
|
|
28
30
|
broader local-primary sync workflow is still being built on top of it.
|
|
31
|
+
The lane-aware session workflow is intended to publish as `0.2.0`; the local
|
|
32
|
+
package may remain at `0.1.0` until the publish pass.
|
|
29
33
|
|
|
30
34
|
## Requirements
|
|
31
35
|
|
|
@@ -39,6 +43,9 @@ broader local-primary sync workflow is still being built on top of it.
|
|
|
39
43
|
npx -y @vibecompass/vibecompass --help
|
|
40
44
|
```
|
|
41
45
|
|
|
46
|
+
The npm package is scoped under the VibeCompass org, but the installed CLI
|
|
47
|
+
command is intentionally unscoped: `vibecompass`.
|
|
48
|
+
|
|
42
49
|
### Library
|
|
43
50
|
|
|
44
51
|
```bash
|
|
@@ -98,6 +105,7 @@ If you do not chain straight into a first session from `init`, open one later:
|
|
|
98
105
|
|
|
99
106
|
```bash
|
|
100
107
|
npx -y @vibecompass/vibecompass start-session \
|
|
108
|
+
--id mcp-dogfood \
|
|
101
109
|
--working-on "Validate the MCP dogfood workflow against the docs repo"
|
|
102
110
|
```
|
|
103
111
|
|
|
@@ -105,29 +113,49 @@ Close the active builder session:
|
|
|
105
113
|
|
|
106
114
|
```bash
|
|
107
115
|
npx -y @vibecompass/vibecompass close-session \
|
|
116
|
+
--session mcp-dogfood \
|
|
108
117
|
--title "Workflow Parity Commands" \
|
|
109
118
|
--completed "Added package-owned start-session and close-session commands" \
|
|
110
119
|
--next-step "Run the package publish dry-run"
|
|
111
120
|
```
|
|
112
121
|
|
|
122
|
+
`end-session` is accepted as an alias for the same lifecycle step and flags:
|
|
123
|
+
`npx -y @vibecompass/vibecompass end-session ...`.
|
|
124
|
+
|
|
113
125
|
The package CLI owns filesystem lifecycle only. The default scaffolded
|
|
114
126
|
workflow also uses prompt commands inside your coding tool:
|
|
115
127
|
|
|
116
128
|
```text
|
|
117
129
|
start session # builder role trigger
|
|
118
130
|
join as reviewer # reviewer role trigger
|
|
119
|
-
review handoff # reviewer reads
|
|
120
|
-
address review # builder reads
|
|
131
|
+
review handoff # reviewer reads selected-lane wip.md, handoff.md, and diffs
|
|
132
|
+
address review # builder reads selected-lane feedback, responds inline, and continues
|
|
121
133
|
```
|
|
122
134
|
|
|
123
135
|
Those prompt commands are documented in the generated `context.md`,
|
|
124
136
|
`CLAUDE.md`, and `AGENTS.md` templates. They are not additional
|
|
125
137
|
`vibecompass` CLI subcommands.
|
|
126
138
|
Reviewer handback is explicit in the package-managed workflow: the reviewer
|
|
127
|
-
ends the pass by updating `wip.md` + `handoff.md`, and
|
|
128
|
-
the session with `vibecompass close-session
|
|
139
|
+
ends the pass by updating the selected lane's `wip.md` + `handoff.md`, and
|
|
140
|
+
the builder closes the session with `vibecompass close-session --session
|
|
141
|
+
<lane-id>` using the defaults stored in
|
|
129
142
|
`project.yaml.metadata.workflow.close_session`.
|
|
130
143
|
|
|
144
|
+
Active builder sessions are named lanes under
|
|
145
|
+
`sessions/active/<lane-id>/`. Use one lane per active feature or workstream.
|
|
146
|
+
`start-session` without `--id` opens the compatibility `default` lane only
|
|
147
|
+
when no other lanes are active; concurrent work should pass `--id`.
|
|
148
|
+
Finalized sessions are append-only notes named
|
|
149
|
+
`sessions/YYYY-MM-DD-N-title.md`, so multiple sessions on the same day
|
|
150
|
+
increment `N`. Decisions remain append-only in `decisions/`; a session note
|
|
151
|
+
can reference a decision, but the decision file is the durable source of truth.
|
|
152
|
+
|
|
153
|
+
For higher-risk work, use planning mode as a prompt-level step inside the
|
|
154
|
+
active session. Planning mode should read the same context, propose scope and
|
|
155
|
+
tradeoffs, and record the agreed plan in the selected lane's `wip.md`; it
|
|
156
|
+
should not create a separate lifecycle artifact or finalize decisions until
|
|
157
|
+
you approve implementation.
|
|
158
|
+
|
|
131
159
|
Generate agent-instruction files from project memory:
|
|
132
160
|
|
|
133
161
|
```bash
|
|
@@ -227,10 +255,31 @@ Options:
|
|
|
227
255
|
- `--root <path>`: project-memory root; defaults to `.compass`
|
|
228
256
|
- `--tooling-root <path>`: workspace root that contains `CLAUDE.md`; defaults to cwd
|
|
229
257
|
- `--working-on <text>`: required active-session summary
|
|
258
|
+
- `--id <lane-id>`: optional lane ID; defaults to `default` only when no lanes are active
|
|
259
|
+
- `--feature <slug>`: repeatable feature slug for the lane
|
|
260
|
+
- `--repo <id>`: repeatable repo ID for the lane
|
|
261
|
+
- `--claim <path>`: repeatable path claim for overlap warnings
|
|
230
262
|
- `--date <YYYY-MM-DD>`: optional explicit session date
|
|
231
263
|
|
|
232
264
|
This command updates the `Current session` block in `CLAUDE.md` and creates
|
|
233
|
-
`sessions/wip.md
|
|
265
|
+
`sessions/active/<lane-id>/session.yaml`, `wip.md`, and `handoff.md` for the
|
|
266
|
+
next session number. It also updates `sessions/active/index.yaml`.
|
|
267
|
+
|
|
268
|
+
### `vibecompass list-sessions`
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
vibecompass list-sessions [options]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Lists active session lanes and marks the current lane.
|
|
275
|
+
|
|
276
|
+
### `vibecompass switch-session`
|
|
277
|
+
|
|
278
|
+
```text
|
|
279
|
+
vibecompass switch-session <lane-id> [options]
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Changes the current lane in `sessions/active/index.yaml`.
|
|
234
283
|
|
|
235
284
|
### `vibecompass close-session`
|
|
236
285
|
|
|
@@ -238,12 +287,17 @@ This command updates the `Current session` block in `CLAUDE.md` and creates
|
|
|
238
287
|
vibecompass close-session --title <text> --completed <text> --next-step <text> [options]
|
|
239
288
|
```
|
|
240
289
|
|
|
290
|
+
`vibecompass end-session` accepts the same options and runs the same close
|
|
291
|
+
path. `close-session` remains the canonical command name in generated workflow
|
|
292
|
+
metadata and docs.
|
|
293
|
+
|
|
241
294
|
Options:
|
|
242
295
|
|
|
243
296
|
- `--root <path>`: project-memory root; defaults to `.compass`
|
|
244
297
|
- `--tooling-root <path>`: workspace root that contains `CLAUDE.md`; defaults to cwd
|
|
245
298
|
- `--title <text>`: required finalized session-note title
|
|
246
299
|
- `--worked-on <text>`: optional override for the "What we worked on" section
|
|
300
|
+
- `--session <lane-id>`: active lane to close; required when multiple lanes are active
|
|
247
301
|
- `--completed <text>`: repeatable completed item
|
|
248
302
|
- `--decision <text>`: repeatable decision reference or summary
|
|
249
303
|
- `--model <text>`: optional repeatable model contribution entry
|
|
@@ -251,7 +305,8 @@ Options:
|
|
|
251
305
|
- `--next-step <text>`: repeatable next-session instruction
|
|
252
306
|
|
|
253
307
|
This command finalizes the active scratchpad into
|
|
254
|
-
`sessions/YYYY-MM-DD-N-display-title.md`, deletes
|
|
308
|
+
`sessions/YYYY-MM-DD-N-display-title.md`, deletes the closed lane directory
|
|
309
|
+
under `sessions/active/<lane-id>/`, updates `sessions/active/index.yaml`, and
|
|
255
310
|
updates the `Current session` block in `CLAUDE.md`. If no `--model` flags are
|
|
256
311
|
provided, the session note records `Not recorded.` under `Models used`.
|
|
257
312
|
This is the final builder lifecycle step after the last `address review`
|
|
@@ -283,15 +338,15 @@ These are the default prompt commands used by the scaffolded workflow files:
|
|
|
283
338
|
|
|
284
339
|
- `start session`: builder role trigger
|
|
285
340
|
- `join as reviewer`: reviewer role trigger
|
|
286
|
-
- `review handoff`: reviewer reads the
|
|
287
|
-
- `address review`: builder reads the latest reviewer feedback in `wip.md` / `handoff.md`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
341
|
+
- `review handoff`: reviewer reads the selected lane's latest `wip.md`, `handoff.md`, latest finalized note, and relevant diffs before appending findings
|
|
342
|
+
- `address review`: builder reads the latest reviewer feedback in the selected lane's `wip.md` / `handoff.md`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
288
343
|
|
|
289
344
|
They are prompt commands for AI-tool behavior, not package CLI commands.
|
|
290
|
-
Reviewer handback ends when the reviewer has updated
|
|
291
|
-
`handoff.md`; there is no extra reviewer-exit step. The builder
|
|
292
|
-
in builder role, uses `address review` until findings are resolved or
|
|
293
|
-
explicitly deferred, and then runs `vibecompass close-session
|
|
294
|
-
workflow defaults recorded in `project.yaml`.
|
|
345
|
+
Reviewer handback ends when the reviewer has updated the selected lane's
|
|
346
|
+
`wip.md` and `handoff.md`; there is no extra reviewer-exit step. The builder
|
|
347
|
+
remains in builder role, uses `address review` until findings are resolved or
|
|
348
|
+
explicitly deferred, and then runs `vibecompass close-session --session
|
|
349
|
+
<lane-id>` using the workflow defaults recorded in `project.yaml`.
|
|
295
350
|
|
|
296
351
|
## JavaScript API
|
|
297
352
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { realpathSync } from 'node:fs';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { initializeProjectMemory } from './init.js';
|
|
6
6
|
import { resolveInitCliOptions } from './setup.js';
|
|
7
|
-
import { closeProjectSession, startProjectSession } from './session.js';
|
|
7
|
+
import { closeProjectSession, listProjectSessions, startProjectSession, switchProjectSession } from './session.js';
|
|
8
8
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
9
9
|
|
|
10
10
|
export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
@@ -86,7 +86,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
86
86
|
return 0;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
if (parsed.command === 'close-session') {
|
|
89
|
+
if (parsed.command === 'close-session' || parsed.command === 'end-session') {
|
|
90
90
|
const result = await closeProjectSession({
|
|
91
91
|
...parsed.options,
|
|
92
92
|
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
@@ -104,6 +104,32 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
104
104
|
return 0;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
if (parsed.command === 'list-sessions') {
|
|
108
|
+
const result = await listProjectSessions({
|
|
109
|
+
...parsed.options,
|
|
110
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
111
|
+
});
|
|
112
|
+
io.stdout.write(`Active session lanes${result.current ? ` (current: ${result.current})` : ''}:\n`);
|
|
113
|
+
if (result.lanes.length === 0) {
|
|
114
|
+
io.stdout.write('- None\n');
|
|
115
|
+
} else {
|
|
116
|
+
for (const lane of result.lanes) {
|
|
117
|
+
const marker = lane.id === result.current ? '*' : '-';
|
|
118
|
+
io.stdout.write(`${marker} ${lane.id}: ${lane.workingOn ?? 'No summary recorded'}\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (parsed.command === 'switch-session') {
|
|
125
|
+
const result = await switchProjectSession({
|
|
126
|
+
...parsed.options,
|
|
127
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
128
|
+
});
|
|
129
|
+
io.stdout.write(`Current session lane: ${result.current}\n`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
if (parsed.command === 'sync-agents') {
|
|
108
134
|
const result = await syncAgentInstructionFiles({
|
|
109
135
|
...parsed.options,
|
|
@@ -128,10 +154,18 @@ export function parseCliArgs(argv) {
|
|
|
128
154
|
return parseStartSessionArgs(rest);
|
|
129
155
|
}
|
|
130
156
|
|
|
131
|
-
if (command === 'close-session') {
|
|
157
|
+
if (command === 'close-session' || command === 'end-session') {
|
|
132
158
|
return parseCloseSessionArgs(rest);
|
|
133
159
|
}
|
|
134
160
|
|
|
161
|
+
if (command === 'list-sessions') {
|
|
162
|
+
return parseListSessionsArgs(rest);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (command === 'switch-session') {
|
|
166
|
+
return parseSwitchSessionArgs(rest);
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
if (command === 'sync-agents') {
|
|
136
170
|
return parseSyncAgentsArgs(rest);
|
|
137
171
|
}
|
|
@@ -322,6 +356,18 @@ function parseStartSessionArgs(argv) {
|
|
|
322
356
|
case '--working-on':
|
|
323
357
|
parsed.workingOn = value;
|
|
324
358
|
break;
|
|
359
|
+
case '--id':
|
|
360
|
+
parsed.sessionId = value;
|
|
361
|
+
break;
|
|
362
|
+
case '--feature':
|
|
363
|
+
parsed.features = [...(parsed.features ?? []), value];
|
|
364
|
+
break;
|
|
365
|
+
case '--repo':
|
|
366
|
+
parsed.repos = [...(parsed.repos ?? []), value];
|
|
367
|
+
break;
|
|
368
|
+
case '--claim':
|
|
369
|
+
parsed.claims = [...(parsed.claims ?? []), value];
|
|
370
|
+
break;
|
|
325
371
|
case '--date':
|
|
326
372
|
parsed.date = value;
|
|
327
373
|
break;
|
|
@@ -381,6 +427,9 @@ function parseCloseSessionArgs(argv) {
|
|
|
381
427
|
case '--worked-on':
|
|
382
428
|
parsed.workedOn = value;
|
|
383
429
|
break;
|
|
430
|
+
case '--session':
|
|
431
|
+
parsed.sessionId = value;
|
|
432
|
+
break;
|
|
384
433
|
case '--completed':
|
|
385
434
|
parsed.completed.push(value);
|
|
386
435
|
break;
|
|
@@ -413,6 +462,80 @@ function parseCloseSessionArgs(argv) {
|
|
|
413
462
|
};
|
|
414
463
|
}
|
|
415
464
|
|
|
465
|
+
function parseListSessionsArgs(argv) {
|
|
466
|
+
const parsed = {};
|
|
467
|
+
|
|
468
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
469
|
+
const token = argv[index];
|
|
470
|
+
if (!token.startsWith('--')) {
|
|
471
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const value = argv[index + 1];
|
|
475
|
+
if (value === undefined) {
|
|
476
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
477
|
+
}
|
|
478
|
+
index += 1;
|
|
479
|
+
|
|
480
|
+
switch (token) {
|
|
481
|
+
case '--root':
|
|
482
|
+
parsed.rootDir = value;
|
|
483
|
+
break;
|
|
484
|
+
default:
|
|
485
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
command: 'list-sessions',
|
|
491
|
+
options: parsed,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function parseSwitchSessionArgs(argv) {
|
|
496
|
+
const parsed = {};
|
|
497
|
+
const positional = [];
|
|
498
|
+
|
|
499
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
500
|
+
const token = argv[index];
|
|
501
|
+
|
|
502
|
+
if (!token.startsWith('--')) {
|
|
503
|
+
positional.push(token);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const value = argv[index + 1];
|
|
508
|
+
if (value === undefined) {
|
|
509
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
510
|
+
}
|
|
511
|
+
index += 1;
|
|
512
|
+
|
|
513
|
+
switch (token) {
|
|
514
|
+
case '--root':
|
|
515
|
+
parsed.rootDir = value;
|
|
516
|
+
break;
|
|
517
|
+
case '--session':
|
|
518
|
+
parsed.sessionId = value;
|
|
519
|
+
break;
|
|
520
|
+
default:
|
|
521
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (positional.length > 1) {
|
|
526
|
+
throw new Error(`Unexpected argument "${positional[1]}".`);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!parsed.sessionId && positional[0]) {
|
|
530
|
+
parsed.sessionId = positional[0];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
command: 'switch-session',
|
|
535
|
+
options: parsed,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
416
539
|
function parseSyncAgentsArgs(argv) {
|
|
417
540
|
const parsed = {};
|
|
418
541
|
|
|
@@ -483,6 +606,9 @@ function usageText() {
|
|
|
483
606
|
' vibecompass init --name <project-name> --mode <local-only|local-primary|hosted-only> --repo <id=remote> [options]',
|
|
484
607
|
' vibecompass start-session --working-on <text> [options]',
|
|
485
608
|
' vibecompass close-session --title <text> --completed <text> --next-step <text> [options]',
|
|
609
|
+
' vibecompass end-session --title <text> --completed <text> --next-step <text> [options] # alias',
|
|
610
|
+
' vibecompass list-sessions [options]',
|
|
611
|
+
' vibecompass switch-session <id> [options]',
|
|
486
612
|
' vibecompass sync-agents [options]',
|
|
487
613
|
'',
|
|
488
614
|
'Init options:',
|
|
@@ -511,16 +637,21 @@ function usageText() {
|
|
|
511
637
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
512
638
|
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
|
|
513
639
|
' --working-on <text> Required active-session summary',
|
|
640
|
+
' --id <lane-id> Optional active session lane ID. Defaults to default when no lanes exist',
|
|
641
|
+
' --feature <slug> Repeatable feature slug for the lane',
|
|
642
|
+
' --repo <id> Repeatable repo ID for the lane',
|
|
643
|
+
' --claim <path> Repeatable path claim for overlap warnings',
|
|
514
644
|
' --date <YYYY-MM-DD> Optional explicit session date',
|
|
515
645
|
' --last-thing-completed <text> Optional override for the CLAUDE.md current-session block',
|
|
516
646
|
' --blockers <text> Optional current blockers summary',
|
|
517
647
|
' --next-session-should <text> Optional current-session handoff summary',
|
|
518
648
|
'',
|
|
519
|
-
'Close-session options:',
|
|
649
|
+
'Close-session options (also accepted by end-session):',
|
|
520
650
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
521
651
|
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
|
|
522
652
|
' --title <text> Required display title for the finalized session note',
|
|
523
653
|
' --worked-on <text> Optional override for "What we worked on"',
|
|
654
|
+
' --session <lane-id> Active session lane to close',
|
|
524
655
|
' --completed <text> Repeatable completed item',
|
|
525
656
|
' --decision <text> Repeatable decision reference or summary',
|
|
526
657
|
' --model <text> Optional repeatable model contribution entry',
|
|
@@ -529,6 +660,10 @@ function usageText() {
|
|
|
529
660
|
' --last-thing-completed <text> Optional override for the CLAUDE.md completed summary',
|
|
530
661
|
' --next-session-should <text> Optional override for the CLAUDE.md next-session summary',
|
|
531
662
|
'',
|
|
663
|
+
'List/switch-session options:',
|
|
664
|
+
' --root <path> Project-memory root. Defaults to .compass',
|
|
665
|
+
' --session <lane-id> Lane ID for switch-session; positional ID is also accepted',
|
|
666
|
+
'',
|
|
532
667
|
'Sync-agents options:',
|
|
533
668
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
534
669
|
' --tooling-root <path> Directory where agent files are written. Defaults to cwd',
|
|
@@ -26,16 +26,25 @@ export function renderSharedInstructionBody(context, options = {}) {
|
|
|
26
26
|
`- Project memory root: \`${context.rootDir}\``,
|
|
27
27
|
'- Read `project.yaml` for project identity, repos, mode, and workflow defaults.',
|
|
28
28
|
'- Read the latest finalized session note under `sessions/` before resuming work.',
|
|
29
|
-
'- Read `sessions/
|
|
29
|
+
'- Read `sessions/active/index.yaml` when it exists, then read the selected lane under `sessions/active/<lane-id>/`.',
|
|
30
|
+
'- Read the selected lane `wip.md` and `handoff.md` when they exist; they are the active builder/reviewer scratch files.',
|
|
30
31
|
'- Read relevant `architecture/` and `decisions/` files before changing implementation.',
|
|
31
32
|
'',
|
|
32
33
|
'## Project Shape',
|
|
33
34
|
...renderProjectShape(context),
|
|
34
35
|
'',
|
|
35
36
|
'## Workflow Rules',
|
|
37
|
+
'- VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream; finalized session notes are append-only history under `sessions/YYYY-MM-DD-N-title.md`.',
|
|
36
38
|
'- Use `start session` for builder work and `join as reviewer` for review work.',
|
|
39
|
+
'- Use `vibecompass start-session --id <lane-id>` for concurrent lanes. Omitting `--id` opens the compatibility `default` lane only when no other lanes are active.',
|
|
40
|
+
'- Use `vibecompass list-sessions` and `vibecompass switch-session <lane-id>` to inspect or change the current lane.',
|
|
41
|
+
'- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
|
|
42
|
+
'- Use optional planning mode for risky or ambiguous work: read the same context, propose scope, and record the agreed plan in the selected lane `wip.md` before implementation.',
|
|
37
43
|
'- Use `review handoff` to ask a reviewer to inspect the active handoff.',
|
|
38
|
-
'- Use `address review` to process reviewer feedback from `
|
|
44
|
+
'- Use `address review` to process reviewer feedback from the selected lane `wip.md` and `handoff.md`.',
|
|
45
|
+
'- End the active builder session with `vibecompass close-session --session <lane-id>`; `vibecompass end-session` is an accepted alias.',
|
|
46
|
+
'- If stale scratch files block `start-session`, read them first, then close, recover, move, or delete them intentionally before retrying.',
|
|
47
|
+
'- Keep decisions append-only in `decisions/`. Session notes may reference decisions, but decisions are the durable source of truth.',
|
|
39
48
|
'- Treat generated agent files as views. Update canonical VibeCompass project memory instead of editing managed regions.',
|
|
40
49
|
'',
|
|
41
50
|
'## Recent Decisions',
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { STATE_VERSION, generateStateManifest, writeStateManifest } from './manifest.js';
|
|
2
2
|
export { initializeProjectMemory } from './init.js';
|
|
3
3
|
export { scanProjectMemory } from './project-memory.js';
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
closeProjectSession,
|
|
6
|
+
listProjectSessions,
|
|
7
|
+
startProjectSession,
|
|
8
|
+
switchProjectSession,
|
|
9
|
+
} from './session.js';
|
|
5
10
|
export {
|
|
6
11
|
syncAgentInstructionFiles,
|
|
7
12
|
getSupportedAgentFormats,
|
package/src/scaffold.js
CHANGED
|
@@ -118,8 +118,18 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
118
118
|
## Derived and scratch files
|
|
119
119
|
- \`${rootRelativePath}/context.md\` — generated AI-facing workflow context
|
|
120
120
|
- \`${rootRelativePath}/state/manifest.json\` — machine-owned local state; do not hand-edit
|
|
121
|
-
- \`${rootRelativePath}/sessions/
|
|
122
|
-
- \`${rootRelativePath}/sessions/
|
|
121
|
+
- \`${rootRelativePath}/sessions/active/index.yaml\` — active session lane index and current lane pointer
|
|
122
|
+
- \`${rootRelativePath}/sessions/active/<lane-id>/session.yaml\` — lane metadata
|
|
123
|
+
- \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\` — lane-local builder scratchpad
|
|
124
|
+
- \`${rootRelativePath}/sessions/active/<lane-id>/handoff.md\` — lane-local builder/reviewer relay
|
|
125
|
+
|
|
126
|
+
## Session model
|
|
127
|
+
- VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream.
|
|
128
|
+
- \`vibecompass start-session\` without \`--id\` opens the compatibility \`default\` lane only when no other lanes are active; use \`--id <lane-id>\` for concurrent work.
|
|
129
|
+
- The active lane scratch files live under \`${rootRelativePath}/sessions/active/<lane-id>/\`.
|
|
130
|
+
- \`${rootRelativePath}/sessions/active/index.yaml\` is the authoritative current lane pointer; the tool-specific Current session block is a human-readable continuity summary, not the lane-selection source of truth.
|
|
131
|
+
- Finalized sessions are append-only notes named \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; multiple sessions on the same day increment \`N\`.
|
|
132
|
+
- Decisions remain append-only and independent from session notes. A session note may reference decisions, but the decision entry in \`${rootRelativePath}/decisions/\` is the durable source of truth.
|
|
123
133
|
|
|
124
134
|
## Repos in scope
|
|
125
135
|
${repos}
|
|
@@ -133,11 +143,13 @@ ${repos}
|
|
|
133
143
|
## Session prompt commands
|
|
134
144
|
- \`start session\` — builder role trigger
|
|
135
145
|
- \`join as reviewer\` — reviewer role trigger
|
|
136
|
-
- \`
|
|
137
|
-
- \`
|
|
146
|
+
- \`planning mode\` — optional prompt-level mode for scoping work before implementation
|
|
147
|
+
- \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
|
|
148
|
+
- \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
138
149
|
|
|
139
150
|
These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
|
|
140
|
-
Reviewer handback is explicit: the reviewer ends the pass by updating \`wip.md\` + \`handoff.md\` and then stopping. Builder close-out uses \`vibecompass close-session
|
|
151
|
+
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. Builder close-out uses \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${rootRelativePath}/project.yaml\`.
|
|
152
|
+
\`vibecompass end-session\` is also accepted as an alias for \`vibecompass close-session\`; the canonical command name remains \`close-session\`.
|
|
141
153
|
|
|
142
154
|
## Workflow defaults
|
|
143
155
|
${renderWorkflowDefaults(workflow)}
|
|
@@ -145,17 +157,20 @@ ${renderWorkflowDefaults(workflow)}
|
|
|
145
157
|
## Session startup
|
|
146
158
|
1. Read \`${rootRelativePath}/project.yaml\`.
|
|
147
159
|
2. Read the latest finalized session note in \`${rootRelativePath}/sessions/\`.
|
|
148
|
-
3. If present, read \`${rootRelativePath}/sessions/
|
|
149
|
-
4. If present, read \`${rootRelativePath}/sessions/
|
|
150
|
-
5.
|
|
160
|
+
3. If present, read \`${rootRelativePath}/sessions/active/index.yaml\` and choose the selected or current lane.
|
|
161
|
+
4. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\`.
|
|
162
|
+
5. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/handoff.md\`.
|
|
163
|
+
6. Read the relevant docs under \`${rootRelativePath}/architecture/\` and \`${rootRelativePath}/decisions/\`.
|
|
151
164
|
|
|
152
165
|
## Builder workflow
|
|
153
|
-
At session start, prefer running \`vibecompass start-session --working-on "..." \`.
|
|
154
|
-
If you manage files manually, create \`${rootRelativePath}/sessions/wip.md\` if it does not exist:
|
|
166
|
+
At session start, prefer running \`vibecompass start-session --id <lane-id> --working-on "..." \`. Omit \`--id\` only for the first/default lane in a project with no other active lanes.
|
|
167
|
+
If you manage files manually, create \`${rootRelativePath}/sessions/active/<lane-id>/session.yaml\` and \`${rootRelativePath}/sessions/active/index.yaml\`, then create \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\` if it does not exist:
|
|
155
168
|
|
|
156
169
|
\`\`\`md
|
|
157
170
|
# WIP — YYYY-MM-DD (session N)
|
|
158
171
|
|
|
172
|
+
Session lane: <lane-id>
|
|
173
|
+
|
|
159
174
|
## Working on
|
|
160
175
|
|
|
161
176
|
## Log
|
|
@@ -165,11 +180,13 @@ If you manage files manually, create \`${rootRelativePath}/sessions/wip.md\` if
|
|
|
165
180
|
## Review log
|
|
166
181
|
\`\`\`
|
|
167
182
|
|
|
168
|
-
Also create \`${rootRelativePath}/sessions/handoff.md\` if it does not exist:
|
|
183
|
+
Also create \`${rootRelativePath}/sessions/active/<lane-id>/handoff.md\` if it does not exist:
|
|
169
184
|
|
|
170
185
|
\`\`\`md
|
|
171
186
|
# Handoff — YYYY-MM-DD (session N)
|
|
172
187
|
|
|
188
|
+
Session lane: <lane-id>
|
|
189
|
+
|
|
173
190
|
## Builder → Reviewer
|
|
174
191
|
|
|
175
192
|
### What changed
|
|
@@ -188,20 +205,29 @@ Also create \`${rootRelativePath}/sessions/handoff.md\` if it does not exist:
|
|
|
188
205
|
During the session:
|
|
189
206
|
- append short summaries to \`wip.md\` after meaningful exchanges
|
|
190
207
|
- keep \`handoff.md\` current after substantive work blocks
|
|
191
|
-
- use \`
|
|
192
|
-
-
|
|
208
|
+
- use \`vibecompass list-sessions\` and \`vibecompass switch-session <lane-id>\` to inspect or change the current lane
|
|
209
|
+
- use \`address review\` when reviewer feedback lands so the builder resolves it from the selected lane's latest \`wip.md\` / \`handoff.md\`
|
|
210
|
+
- stay in builder role through close-out; resolve or explicitly defer reviewer feedback before running \`vibecompass close-session --session <lane-id>\`
|
|
193
211
|
- record architectural decisions in \`${rootRelativePath}/decisions/\` before implementing them
|
|
194
212
|
|
|
213
|
+
If \`vibecompass start-session\` reports stale scratch files, read the existing lane-local \`wip.md\` and \`handoff.md\` first. Either close that session normally, recover its useful notes into a finalized session note, or intentionally move/delete the stale scratch files before starting a new session.
|
|
214
|
+
|
|
215
|
+
## Optional planning mode
|
|
216
|
+
- Use planning mode for risky, ambiguous, cross-file, or architectural work before implementation.
|
|
217
|
+
- Planning mode reads the same startup context as builder mode and may update the selected lane's \`wip.md\` with agreed scope, constraints, and open questions.
|
|
218
|
+
- Planning mode should not finalize session notes, mutate decisions, or make broad code changes until the user approves the plan.
|
|
219
|
+
- If planning produces a real architectural decision, append it to \`${rootRelativePath}/decisions/\` before implementing.
|
|
220
|
+
|
|
195
221
|
At session close:
|
|
196
|
-
- prefer running \`vibecompass close-session --title "..." --completed "..." --model "..." --next-step "..."\`
|
|
222
|
+
- prefer running \`vibecompass close-session --session <lane-id> --title "..." --completed "..." --model "..." --next-step "..."\`; \`vibecompass end-session\` is a supported alias
|
|
197
223
|
- follow the stored close-session defaults from \`${rootRelativePath}/project.yaml\`
|
|
198
|
-
- finalize \`wip.md\` into \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`
|
|
199
|
-
- delete
|
|
224
|
+
- finalize the lane-local \`wip.md\` into \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`
|
|
225
|
+
- delete the closed lane directory under \`${rootRelativePath}/sessions/active/<lane-id>/\`
|
|
200
226
|
- refresh any affected architecture/decision docs
|
|
201
227
|
|
|
202
228
|
## Reviewer workflow
|
|
203
229
|
- use \`review handoff\` when you want the reviewer to run the next review pass
|
|
204
|
-
- read the latest finalized session note, then \`wip.md\`, then \`handoff.md\`
|
|
230
|
+
- read the latest finalized session note, then the selected lane's \`wip.md\`, then \`handoff.md\`
|
|
205
231
|
- inspect the relevant code/docs diffs
|
|
206
232
|
- append findings under \`## Review log\` in \`wip.md\`
|
|
207
233
|
- write a concise baton-pass summary into \`handoff.md\`
|
|
@@ -284,8 +310,10 @@ Finalized session notes for ${projectConfig.name} live here.
|
|
|
284
310
|
- \`## Next session should start with\`
|
|
285
311
|
|
|
286
312
|
## Active-session scratch files
|
|
287
|
-
- \`
|
|
288
|
-
- \`
|
|
313
|
+
- \`active/index.yaml\` — active lane index and current lane pointer
|
|
314
|
+
- \`active/<lane-id>/session.yaml\` — lane metadata
|
|
315
|
+
- \`active/<lane-id>/wip.md\` — builder scratchpad during an active lane
|
|
316
|
+
- \`active/<lane-id>/handoff.md\` — reviewer/builder baton-pass during an active lane
|
|
289
317
|
|
|
290
318
|
Those scratch files are session-scoped working artifacts, not finalized history.
|
|
291
319
|
`;
|
|
@@ -304,16 +332,19 @@ Read \`${options.contextRelativeToToolingRoot}\` before doing substantive work.
|
|
|
304
332
|
|---|---|---|
|
|
305
333
|
| "start session" | Builder | Yes |
|
|
306
334
|
| "join as reviewer" | Reviewer | No |
|
|
335
|
+
| "planning mode" | Planner | No — scopes work inside the selected active lane before implementation |
|
|
307
336
|
|
|
308
337
|
## Prompt commands
|
|
309
338
|
|
|
310
339
|
- \`start session\` — builder role trigger
|
|
311
340
|
- \`join as reviewer\` — reviewer role trigger
|
|
312
|
-
- \`
|
|
313
|
-
- \`
|
|
341
|
+
- \`planning mode\` — optional prompt-level mode for scoping risky or ambiguous work before implementation
|
|
342
|
+
- \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
|
|
343
|
+
- \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
314
344
|
|
|
315
345
|
These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
|
|
316
|
-
Reviewer handback is explicit: the reviewer ends the pass by updating
|
|
346
|
+
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. Builder close-out uses \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
|
|
347
|
+
\`vibecompass end-session\` is accepted as an alias for \`vibecompass close-session\`; \`close-session\` remains the canonical command name.
|
|
317
348
|
|
|
318
349
|
## Current session
|
|
319
350
|
|
|
@@ -330,8 +361,11 @@ Next session should: Read \`${options.contextRelativeToToolingRoot}\`, then open
|
|
|
330
361
|
## Startup
|
|
331
362
|
- Read \`${options.contextRelativeToToolingRoot}\`
|
|
332
363
|
- Follow the builder/reviewer protocol defined there
|
|
333
|
-
- Use \`vibecompass start-session\` and \`vibecompass close-session
|
|
334
|
-
-
|
|
364
|
+
- Use \`vibecompass start-session --id <lane-id>\`, \`vibecompass list-sessions\`, \`vibecompass switch-session <lane-id>\`, and \`vibecompass close-session --session <lane-id>\` to manage active lanes when possible
|
|
365
|
+
- Treat \`${options.rootRelativePath}/sessions/active/index.yaml\` as the current lane source of truth; this Current session block is only a continuity summary
|
|
366
|
+
- Keep the selected lane's \`${options.rootRelativePath}/sessions/active/<lane-id>/wip.md\` and \`${options.rootRelativePath}/sessions/active/<lane-id>/handoff.md\` current during an active session
|
|
367
|
+
- Use planning mode when scope is unclear; record the agreed plan in the selected lane's \`wip.md\` before implementing
|
|
368
|
+
- If stale scratch files block \`start-session\`, read them first, then close, recover, move, or delete them intentionally before retrying
|
|
335
369
|
- Workflow defaults:
|
|
336
370
|
${renderWorkflowDefaults(workflow)}
|
|
337
371
|
`;
|
|
@@ -350,27 +384,33 @@ This workspace uses VibeCompass project memory rooted at \`${options.rootRelativ
|
|
|
350
384
|
|---|---|---|
|
|
351
385
|
| "start session" | Builder | Yes — opens and closes the session lifecycle |
|
|
352
386
|
| "join as reviewer" | Reviewer | No — reviews the builder's work and updates the handoff |
|
|
387
|
+
| "planning mode" | Planner | No — scopes work inside the selected active lane before implementation |
|
|
353
388
|
|
|
354
389
|
## Prompt commands
|
|
355
390
|
|
|
356
391
|
- \`start session\` — builder role trigger
|
|
357
392
|
- \`join as reviewer\` — reviewer role trigger
|
|
358
|
-
- \`
|
|
359
|
-
- \`
|
|
393
|
+
- \`planning mode\` — optional prompt-level mode for scoping risky or ambiguous work before implementation
|
|
394
|
+
- \`review handoff\` — reviewer reads the selected lane's \`wip.md\`, \`handoff.md\`, latest finalized note, and relevant diffs before appending findings
|
|
395
|
+
- \`address review\` — builder reads reviewer feedback in the selected lane's \`wip.md\` / \`handoff.md\`, responds inline, applies accepted changes, and refreshes the builder handoff
|
|
360
396
|
|
|
361
397
|
These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
|
|
362
|
-
Reviewer handback is explicit: the reviewer ends the pass by updating
|
|
398
|
+
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. Builder close-out uses \`vibecompass close-session --session <lane-id>\`, which follows the workflow defaults recorded in \`${options.rootRelativePath}/project.yaml\`.
|
|
399
|
+
\`vibecompass end-session\` is accepted as an alias for \`vibecompass close-session\`; \`close-session\` remains the canonical command name.
|
|
363
400
|
|
|
364
401
|
## Startup
|
|
365
402
|
Before doing substantive work:
|
|
366
403
|
1. Read \`${options.contextRelativeToToolingRoot}\`.
|
|
367
404
|
2. Read the latest finalized session note under \`${options.rootRelativePath}/sessions/\`.
|
|
368
|
-
3. If present, read \`${options.rootRelativePath}/sessions/
|
|
369
|
-
4. If present, read \`${options.rootRelativePath}/sessions/
|
|
370
|
-
5.
|
|
405
|
+
3. If present, read \`${options.rootRelativePath}/sessions/active/index.yaml\` and choose the selected or current lane.
|
|
406
|
+
4. If present, read \`${options.rootRelativePath}/sessions/active/<lane-id>/wip.md\`.
|
|
407
|
+
5. If present, read \`${options.rootRelativePath}/sessions/active/<lane-id>/handoff.md\`.
|
|
408
|
+
6. Read the relevant docs under \`${options.rootRelativePath}/architecture/\` and \`${options.rootRelativePath}/decisions/\`.
|
|
371
409
|
|
|
372
410
|
## Working rule
|
|
373
411
|
Treat \`${options.contextRelativeToToolingRoot}\` as the local workflow source of truth for the builder/reviewer protocol and session scratch-file structure. When the package provides \`start-session\` / \`close-session\`, prefer those commands over hand-editing boilerplate.
|
|
412
|
+
Use planning mode when scope is unclear; record the agreed plan in the selected lane's \`wip.md\` before implementing.
|
|
413
|
+
If stale scratch files block \`start-session\`, read them first, then close, recover, move, or delete them intentionally before retrying.
|
|
374
414
|
- Workflow defaults:
|
|
375
415
|
${renderWorkflowDefaults(workflow)}
|
|
376
416
|
`;
|
|
@@ -397,7 +437,7 @@ function renderWorkflowDefaults(workflow) {
|
|
|
397
437
|
|
|
398
438
|
function describeReviewerHandback(workflow) {
|
|
399
439
|
if (workflow.reviewerHandback === 'handoff-file') {
|
|
400
|
-
return 'update sessions/wip.md and sessions/handoff.md, then stop the review pass';
|
|
440
|
+
return 'update sessions/active/<lane-id>/wip.md and sessions/active/<lane-id>/handoff.md, then stop the review pass';
|
|
401
441
|
}
|
|
402
442
|
|
|
403
443
|
return workflow.reviewerHandback;
|
package/src/session.js
CHANGED
|
@@ -7,6 +7,25 @@ import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
|
7
7
|
const CURRENT_SESSION_REQUIRED_FIELDS = ['Date:', 'Working on:', 'Last thing completed:', 'Blockers:', 'Next session should:'];
|
|
8
8
|
const WIP_HEADER_PATTERN = /^# WIP — (\d{4}-\d{2}-\d{2}) \(session (\d+)\)$/m;
|
|
9
9
|
const SESSION_FILENAME_PATTERN = /^(\d{4}-\d{2}-\d{2})-(\d+)-([a-z0-9-]+)\.md$/i;
|
|
10
|
+
const LANE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
11
|
+
const RESERVED_LANE_IDS = new Set([
|
|
12
|
+
'active',
|
|
13
|
+
'current',
|
|
14
|
+
'default',
|
|
15
|
+
'false',
|
|
16
|
+
'handoff',
|
|
17
|
+
'index',
|
|
18
|
+
'new',
|
|
19
|
+
'no',
|
|
20
|
+
'null',
|
|
21
|
+
'off',
|
|
22
|
+
'on',
|
|
23
|
+
'sessions',
|
|
24
|
+
'state',
|
|
25
|
+
'true',
|
|
26
|
+
'wip',
|
|
27
|
+
'yes',
|
|
28
|
+
]);
|
|
10
29
|
|
|
11
30
|
export async function startProjectSession(options) {
|
|
12
31
|
const normalized = normalizeSessionPaths(options);
|
|
@@ -15,18 +34,14 @@ export async function startProjectSession(options) {
|
|
|
15
34
|
|
|
16
35
|
await ensureInitializedProjectMemory(normalized.rootDir);
|
|
17
36
|
const claude = await readClaudeFile(normalized.claudePath);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
throw new Error(
|
|
21
|
-
`An active session scratch file already exists under ${normalized.sessionsDir}. Close or recover the existing session before starting a new one.`,
|
|
22
|
-
);
|
|
23
|
-
}
|
|
37
|
+
const sessionId = await resolveStartSessionId(normalized, options);
|
|
38
|
+
const lanePaths = getLanePaths(normalized, sessionId);
|
|
24
39
|
|
|
25
40
|
const nextSessionNumber = await getNextSessionNumber(normalized.sessionsDir, sessionDate);
|
|
26
41
|
const currentSession = parseCurrentSessionBlock(claude.content);
|
|
27
42
|
const updatedClaude = replaceCurrentSessionBlock(claude.content, {
|
|
28
|
-
date: `${sessionDate} (session ${nextSessionNumber})`,
|
|
29
|
-
workingOn
|
|
43
|
+
date: `${sessionDate} (session ${nextSessionNumber}, lane ${sessionId})`,
|
|
44
|
+
workingOn: `${workingOn} [${sessionId}]`,
|
|
30
45
|
lastThingCompleted:
|
|
31
46
|
normalizeOptionalString(options?.lastThingCompleted) ??
|
|
32
47
|
currentSession.lastThingCompleted ??
|
|
@@ -37,33 +52,62 @@ export async function startProjectSession(options) {
|
|
|
37
52
|
'Finish the active session and close it with a finalized session note.',
|
|
38
53
|
});
|
|
39
54
|
|
|
40
|
-
await mkdir(normalized.
|
|
55
|
+
await mkdir(normalized.activeSessionsDir, { recursive: true });
|
|
56
|
+
await mkdir(lanePaths.laneDir, { recursive: false }).catch((error) => {
|
|
57
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST') {
|
|
58
|
+
throw new Error(`Active session lane "${sessionId}" already exists. Choose a different --id or close that lane first.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
41
63
|
await writeFile(normalized.claudePath, updatedClaude, 'utf8');
|
|
42
64
|
await writeFile(
|
|
43
|
-
|
|
65
|
+
lanePaths.sessionFilePath,
|
|
66
|
+
renderLaneMetadata({
|
|
67
|
+
sessionId,
|
|
68
|
+
sessionDate,
|
|
69
|
+
sessionNumber: nextSessionNumber,
|
|
70
|
+
workingOn,
|
|
71
|
+
features: normalizeStringArray(options?.features),
|
|
72
|
+
repos: normalizeStringArray(options?.repos),
|
|
73
|
+
claims: normalizeStringArray(options?.claims),
|
|
74
|
+
}),
|
|
75
|
+
'utf8',
|
|
76
|
+
);
|
|
77
|
+
await writeFile(
|
|
78
|
+
lanePaths.wipFilePath,
|
|
44
79
|
renderWipTemplate({
|
|
45
80
|
sessionDate,
|
|
46
81
|
sessionNumber: nextSessionNumber,
|
|
47
82
|
workingOn,
|
|
83
|
+
sessionId,
|
|
48
84
|
}),
|
|
49
85
|
'utf8',
|
|
50
86
|
);
|
|
51
87
|
await writeFile(
|
|
52
|
-
|
|
88
|
+
lanePaths.handoffFilePath,
|
|
53
89
|
renderHandoffTemplate({
|
|
54
90
|
sessionDate,
|
|
55
91
|
sessionNumber: nextSessionNumber,
|
|
56
92
|
workingOn,
|
|
93
|
+
sessionId,
|
|
57
94
|
}),
|
|
58
95
|
'utf8',
|
|
59
96
|
);
|
|
97
|
+
await upsertActiveSessionIndex(normalized, {
|
|
98
|
+
id: sessionId,
|
|
99
|
+
status: 'active',
|
|
100
|
+
workingOn,
|
|
101
|
+
}, { current: sessionId });
|
|
60
102
|
|
|
61
103
|
return {
|
|
62
104
|
rootDir: normalized.rootDir,
|
|
63
105
|
toolingRootDir: normalized.toolingRootDir,
|
|
64
106
|
claudePath: normalized.claudePath,
|
|
65
|
-
|
|
66
|
-
|
|
107
|
+
sessionId,
|
|
108
|
+
sessionFilePath: lanePaths.sessionFilePath,
|
|
109
|
+
wipFilePath: lanePaths.wipFilePath,
|
|
110
|
+
handoffFilePath: lanePaths.handoffFilePath,
|
|
67
111
|
sessionDate,
|
|
68
112
|
sessionNumber: nextSessionNumber,
|
|
69
113
|
};
|
|
@@ -71,7 +115,7 @@ export async function startProjectSession(options) {
|
|
|
71
115
|
|
|
72
116
|
export async function closeProjectSession(options) {
|
|
73
117
|
const normalized = normalizeSessionPaths(options);
|
|
74
|
-
const title = requireNonEmptyString(options?.title, '
|
|
118
|
+
const title = requireNonEmptyString(options?.title, 'session close requires a non-empty title.');
|
|
75
119
|
const completed = normalizeStringArray(options?.completed);
|
|
76
120
|
const models = normalizeStringArray(options?.models);
|
|
77
121
|
const nextSteps = normalizeStringArray(options?.nextSteps);
|
|
@@ -79,25 +123,27 @@ export async function closeProjectSession(options) {
|
|
|
79
123
|
const blockers = normalizeStringArray(options?.blockers);
|
|
80
124
|
|
|
81
125
|
if (completed.length === 0) {
|
|
82
|
-
throw new Error('
|
|
126
|
+
throw new Error('session close requires at least one completed item.');
|
|
83
127
|
}
|
|
84
128
|
|
|
85
129
|
if (nextSteps.length === 0) {
|
|
86
|
-
throw new Error('
|
|
130
|
+
throw new Error('session close requires at least one next-step entry.');
|
|
87
131
|
}
|
|
88
132
|
|
|
89
133
|
await ensureInitializedProjectMemory(normalized.rootDir, { allowMissingProjectFile: true });
|
|
90
134
|
const claude = await readClaudeFile(normalized.claudePath);
|
|
91
135
|
const workflowSettings = await readWorkflowSettings(normalized.projectFilePath);
|
|
136
|
+
const sessionId = await resolveExistingSessionId(normalized, options);
|
|
137
|
+
const lanePaths = sessionId ? getLanePaths(normalized, sessionId) : normalized;
|
|
92
138
|
|
|
93
|
-
if (!(await fileExists(
|
|
139
|
+
if (!(await fileExists(lanePaths.wipFilePath))) {
|
|
94
140
|
throw new Error(
|
|
95
|
-
`No active session scratchpad exists at ${
|
|
141
|
+
`No active session scratchpad exists at ${lanePaths.wipFilePath}. Start a session before trying to close it.`,
|
|
96
142
|
);
|
|
97
143
|
}
|
|
98
144
|
|
|
99
|
-
const wipContent = await readFile(
|
|
100
|
-
const activeSession = parseActiveSession(wipContent);
|
|
145
|
+
const wipContent = await readFile(lanePaths.wipFilePath, 'utf8');
|
|
146
|
+
const activeSession = parseActiveSession(wipContent, lanePaths.wipFilePath);
|
|
101
147
|
const currentSession = parseCurrentSessionBlock(claude.content);
|
|
102
148
|
const workedOn =
|
|
103
149
|
normalizeOptionalString(options?.workedOn) ??
|
|
@@ -105,12 +151,12 @@ export async function closeProjectSession(options) {
|
|
|
105
151
|
currentSession.workingOn;
|
|
106
152
|
|
|
107
153
|
if (!workedOn) {
|
|
108
|
-
throw new Error('
|
|
154
|
+
throw new Error('session close could not determine what the session worked on. Pass workedOn explicitly.');
|
|
109
155
|
}
|
|
110
156
|
|
|
111
157
|
const sessionSlug = slugifyTitle(title);
|
|
112
158
|
if (!sessionSlug) {
|
|
113
|
-
throw new Error('
|
|
159
|
+
throw new Error('session close title must contain at least one letter or number.');
|
|
114
160
|
}
|
|
115
161
|
|
|
116
162
|
const sessionRelativePath = toPosix(
|
|
@@ -138,10 +184,15 @@ export async function closeProjectSession(options) {
|
|
|
138
184
|
'utf8',
|
|
139
185
|
);
|
|
140
186
|
|
|
141
|
-
const hadHandoff = await fileExists(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await
|
|
187
|
+
const hadHandoff = await fileExists(lanePaths.handoffFilePath);
|
|
188
|
+
if (sessionId) {
|
|
189
|
+
await rm(lanePaths.laneDir, { recursive: true, force: true });
|
|
190
|
+
await removeActiveSessionFromIndex(normalized, sessionId);
|
|
191
|
+
} else {
|
|
192
|
+
await rm(lanePaths.wipFilePath, { force: true });
|
|
193
|
+
if (hadHandoff) {
|
|
194
|
+
await rm(lanePaths.handoffFilePath, { force: true });
|
|
195
|
+
}
|
|
145
196
|
}
|
|
146
197
|
|
|
147
198
|
const updatedClaude = replaceCurrentSessionBlock(claude.content, {
|
|
@@ -166,6 +217,7 @@ export async function closeProjectSession(options) {
|
|
|
166
217
|
toolingRootDir: normalized.toolingRootDir,
|
|
167
218
|
claudePath: normalized.claudePath,
|
|
168
219
|
projectFilePath: normalized.projectFilePath,
|
|
220
|
+
sessionId,
|
|
169
221
|
sessionFilePath,
|
|
170
222
|
sessionRelativePath,
|
|
171
223
|
sessionDate: activeSession.sessionDate,
|
|
@@ -173,12 +225,43 @@ export async function closeProjectSession(options) {
|
|
|
173
225
|
workflowGuidance: buildCloseSessionGuidance(workflowSettings),
|
|
174
226
|
agentFileSync,
|
|
175
227
|
removedScratchFiles: [
|
|
176
|
-
|
|
177
|
-
...(hadHandoff ? [
|
|
228
|
+
lanePaths.wipFilePath,
|
|
229
|
+
...(hadHandoff ? [lanePaths.handoffFilePath] : []),
|
|
178
230
|
],
|
|
179
231
|
};
|
|
180
232
|
}
|
|
181
233
|
|
|
234
|
+
export async function listProjectSessions(options = {}) {
|
|
235
|
+
const normalized = normalizeSessionPaths(options);
|
|
236
|
+
await ensureInitializedProjectMemory(normalized.rootDir, { allowMissingProjectFile: true });
|
|
237
|
+
const index = await readActiveSessionIndex(normalized);
|
|
238
|
+
const lanes = await listActiveSessionLanes(normalized);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
rootDir: normalized.rootDir,
|
|
242
|
+
current: index.current ?? lanes[0]?.id ?? null,
|
|
243
|
+
lanes,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function switchProjectSession(options = {}) {
|
|
248
|
+
const normalized = normalizeSessionPaths(options);
|
|
249
|
+
const sessionId = validateLaneId(requireNonEmptyString(options?.sessionId, 'switch-session requires a session ID.'));
|
|
250
|
+
const lanes = await listActiveSessionLanes(normalized);
|
|
251
|
+
|
|
252
|
+
if (!lanes.some((lane) => lane.id === sessionId)) {
|
|
253
|
+
throw new Error(`Active session lane "${sessionId}" does not exist.`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await upsertActiveSessionIndex(normalized, null, { current: sessionId });
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
rootDir: normalized.rootDir,
|
|
260
|
+
current: sessionId,
|
|
261
|
+
lanes,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
182
265
|
async function syncAgentInstructionFilesSafely(options) {
|
|
183
266
|
try {
|
|
184
267
|
return await syncAgentInstructionFiles(options);
|
|
@@ -203,20 +286,32 @@ async function syncAgentInstructionFilesSafely(options) {
|
|
|
203
286
|
|
|
204
287
|
function normalizeSessionPaths(options) {
|
|
205
288
|
const cwd = options?.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
289
|
+
const rootDir = path.resolve(cwd, options?.rootDir ?? '.compass');
|
|
290
|
+
const toolingRootDir = options?.toolingRootDir
|
|
291
|
+
? path.resolve(cwd, options.toolingRootDir)
|
|
292
|
+
: cwd;
|
|
206
293
|
|
|
207
294
|
return {
|
|
208
|
-
rootDir
|
|
209
|
-
toolingRootDir
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
),
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
295
|
+
rootDir,
|
|
296
|
+
toolingRootDir,
|
|
297
|
+
projectFilePath: path.resolve(rootDir, 'project.yaml'),
|
|
298
|
+
claudePath: path.resolve(toolingRootDir, 'CLAUDE.md'),
|
|
299
|
+
sessionsDir: path.resolve(rootDir, 'sessions'),
|
|
300
|
+
activeSessionsDir: path.resolve(rootDir, 'sessions/active'),
|
|
301
|
+
activeSessionsIndexPath: path.resolve(rootDir, 'sessions/active/index.yaml'),
|
|
302
|
+
wipFilePath: path.resolve(rootDir, 'sessions/wip.md'),
|
|
303
|
+
handoffFilePath: path.resolve(rootDir, 'sessions/handoff.md'),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getLanePaths(normalized, sessionId) {
|
|
308
|
+
const laneDir = path.resolve(normalized.activeSessionsDir, sessionId);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
laneDir,
|
|
312
|
+
sessionFilePath: path.resolve(laneDir, 'session.yaml'),
|
|
313
|
+
wipFilePath: path.resolve(laneDir, 'wip.md'),
|
|
314
|
+
handoffFilePath: path.resolve(laneDir, 'handoff.md'),
|
|
220
315
|
};
|
|
221
316
|
}
|
|
222
317
|
|
|
@@ -304,11 +399,166 @@ function replaceCurrentSessionBlock(content, fields) {
|
|
|
304
399
|
|
|
305
400
|
async function getNextSessionNumber(sessionsDir, sessionDate) {
|
|
306
401
|
const sessions = await listFinalizedSessions(sessionsDir);
|
|
307
|
-
const
|
|
402
|
+
const activeSessions = await listActiveSessionLanes({ sessionsDir, activeSessionsDir: path.join(sessionsDir, 'active') });
|
|
403
|
+
const todaysSessions = [
|
|
404
|
+
...sessions,
|
|
405
|
+
...activeSessions,
|
|
406
|
+
].filter((session) => session.sessionDate === sessionDate);
|
|
308
407
|
const highestNumber = todaysSessions.reduce((max, session) => Math.max(max, session.sessionNumber), 0);
|
|
309
408
|
return highestNumber + 1;
|
|
310
409
|
}
|
|
311
410
|
|
|
411
|
+
async function resolveStartSessionId(normalized, options) {
|
|
412
|
+
const explicitId = normalizeOptionalString(options?.sessionId);
|
|
413
|
+
if (explicitId) {
|
|
414
|
+
return validateLaneId(explicitId);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const lanes = await listActiveSessionLanes(normalized);
|
|
418
|
+
if (lanes.length > 0) {
|
|
419
|
+
throw new Error('Active session lanes already exist. Pass --id to start another lane explicitly.');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if ((await fileExists(normalized.wipFilePath)) || (await fileExists(normalized.handoffFilePath))) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Legacy active session scratch files already exist under ${normalized.sessionsDir}. Close or recover that session before starting a lane.`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return 'default';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function resolveExistingSessionId(normalized, options) {
|
|
432
|
+
const explicitId = normalizeOptionalString(options?.sessionId);
|
|
433
|
+
if (explicitId) {
|
|
434
|
+
return validateLaneId(explicitId);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const lanes = await listActiveSessionLanes(normalized);
|
|
438
|
+
if (lanes.length === 1) {
|
|
439
|
+
return lanes[0].id;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (lanes.length > 1) {
|
|
443
|
+
throw new Error('Multiple active session lanes exist. Pass --session to choose which lane to close.');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function listActiveSessionLanes(normalized) {
|
|
450
|
+
try {
|
|
451
|
+
const entries = await readdir(normalized.activeSessionsDir, { withFileTypes: true });
|
|
452
|
+
const lanes = [];
|
|
453
|
+
for (const entry of entries) {
|
|
454
|
+
if (!entry.isDirectory()) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const sessionId = entry.name;
|
|
459
|
+
const lanePaths = getLanePaths(normalized, sessionId);
|
|
460
|
+
const metadata = await readLaneMetadata(lanePaths.sessionFilePath);
|
|
461
|
+
const wipSession = await readLaneWipSession(lanePaths.wipFilePath);
|
|
462
|
+
lanes.push({
|
|
463
|
+
id: sessionId,
|
|
464
|
+
status: metadata.status ?? 'active',
|
|
465
|
+
workingOn: metadata.workingOn ?? null,
|
|
466
|
+
features: metadata.features,
|
|
467
|
+
repos: metadata.repos,
|
|
468
|
+
claims: metadata.claims,
|
|
469
|
+
sessionDate: metadata.sessionDate ?? wipSession?.sessionDate ?? null,
|
|
470
|
+
sessionNumber: metadata.sessionNumber ?? wipSession?.sessionNumber ?? 0,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return lanes.sort((left, right) => left.id.localeCompare(right.id));
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function readLaneWipSession(wipFilePath) {
|
|
485
|
+
try {
|
|
486
|
+
return parseActiveSession(await readFile(wipFilePath, 'utf8'), wipFilePath);
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function readLaneMetadata(sessionFilePath) {
|
|
493
|
+
try {
|
|
494
|
+
const content = await readFile(sessionFilePath, 'utf8');
|
|
495
|
+
const data = parseSimpleYaml(content, { sourceName: sessionFilePath });
|
|
496
|
+
return {
|
|
497
|
+
status: normalizeOptionalString(data.status),
|
|
498
|
+
workingOn: normalizeOptionalString(data.working_on),
|
|
499
|
+
features: normalizeStringArray(data.feature_slugs),
|
|
500
|
+
repos: normalizeStringArray(data.repos),
|
|
501
|
+
claims: normalizeStringArray(data.claimed_paths),
|
|
502
|
+
sessionDate: normalizeOptionalString(data.session_date),
|
|
503
|
+
sessionNumber: typeof data.session_number === 'number' ? data.session_number : Number(data.session_number) || null,
|
|
504
|
+
};
|
|
505
|
+
} catch {
|
|
506
|
+
return {
|
|
507
|
+
status: null,
|
|
508
|
+
workingOn: null,
|
|
509
|
+
features: [],
|
|
510
|
+
repos: [],
|
|
511
|
+
claims: [],
|
|
512
|
+
sessionDate: null,
|
|
513
|
+
sessionNumber: null,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function readActiveSessionIndex(normalized) {
|
|
519
|
+
try {
|
|
520
|
+
const data = parseSimpleYaml(await readFile(normalized.activeSessionsIndexPath, 'utf8'), {
|
|
521
|
+
sourceName: normalized.activeSessionsIndexPath,
|
|
522
|
+
});
|
|
523
|
+
return {
|
|
524
|
+
current: normalizeOptionalString(data.current),
|
|
525
|
+
};
|
|
526
|
+
} catch {
|
|
527
|
+
return {
|
|
528
|
+
current: null,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function upsertActiveSessionIndex(normalized, lane, options = {}) {
|
|
534
|
+
const currentIndex = await readActiveSessionIndex(normalized);
|
|
535
|
+
const lanes = await listActiveSessionLanes(normalized);
|
|
536
|
+
const laneMap = new Map(lanes.map((item) => [item.id, item]));
|
|
537
|
+
if (lane) {
|
|
538
|
+
laneMap.set(lane.id, {
|
|
539
|
+
id: lane.id,
|
|
540
|
+
status: lane.status,
|
|
541
|
+
workingOn: lane.workingOn,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const current = options.current ?? currentIndex.current ?? lane?.id ?? null;
|
|
546
|
+
await mkdir(normalized.activeSessionsDir, { recursive: true });
|
|
547
|
+
await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, [...laneMap.values()]), 'utf8');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function removeActiveSessionFromIndex(normalized, sessionId) {
|
|
551
|
+
const index = await readActiveSessionIndex(normalized);
|
|
552
|
+
const lanes = (await listActiveSessionLanes(normalized)).filter((lane) => lane.id !== sessionId);
|
|
553
|
+
const current = index.current === sessionId ? lanes[0]?.id ?? null : index.current;
|
|
554
|
+
if (lanes.length === 0) {
|
|
555
|
+
await rm(normalized.activeSessionsIndexPath, { force: true });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, lanes), 'utf8');
|
|
560
|
+
}
|
|
561
|
+
|
|
312
562
|
async function listFinalizedSessions(sessionsDir) {
|
|
313
563
|
try {
|
|
314
564
|
const entries = await readdir(sessionsDir, { withFileTypes: true });
|
|
@@ -337,10 +587,10 @@ async function listFinalizedSessions(sessionsDir) {
|
|
|
337
587
|
}
|
|
338
588
|
}
|
|
339
589
|
|
|
340
|
-
function parseActiveSession(wipContent) {
|
|
590
|
+
function parseActiveSession(wipContent, wipFilePath = 'sessions/active/<lane-id>/wip.md') {
|
|
341
591
|
const match = wipContent.match(WIP_HEADER_PATTERN);
|
|
342
592
|
if (!match) {
|
|
343
|
-
throw new Error(
|
|
593
|
+
throw new Error(`${wipFilePath} is missing the expected "# WIP — YYYY-MM-DD (session N)" header.`);
|
|
344
594
|
}
|
|
345
595
|
|
|
346
596
|
return {
|
|
@@ -349,6 +599,19 @@ function parseActiveSession(wipContent) {
|
|
|
349
599
|
};
|
|
350
600
|
}
|
|
351
601
|
|
|
602
|
+
function validateLaneId(value) {
|
|
603
|
+
const normalized = value.trim();
|
|
604
|
+
if (!LANE_ID_PATTERN.test(normalized)) {
|
|
605
|
+
throw new Error('Session lane ID must be a lowercase slug 3-64 characters long using letters, numbers, and hyphens.');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (RESERVED_LANE_IDS.has(normalized)) {
|
|
609
|
+
throw new Error(`Session lane ID "${normalized}" is reserved.`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return normalized;
|
|
613
|
+
}
|
|
614
|
+
|
|
352
615
|
function extractSectionBody(content, sectionTitle) {
|
|
353
616
|
const heading = `## ${sectionTitle}`.toLowerCase();
|
|
354
617
|
const lines = content.split('\n');
|
|
@@ -407,6 +670,8 @@ function findCurrentSessionFence(content) {
|
|
|
407
670
|
function renderWipTemplate(options) {
|
|
408
671
|
return `# WIP — ${options.sessionDate} (session ${options.sessionNumber})
|
|
409
672
|
|
|
673
|
+
Session lane: ${options.sessionId}
|
|
674
|
+
|
|
410
675
|
## Working on
|
|
411
676
|
${options.workingOn}
|
|
412
677
|
|
|
@@ -423,6 +688,8 @@ ${options.workingOn}
|
|
|
423
688
|
function renderHandoffTemplate(options) {
|
|
424
689
|
return `# Handoff — ${options.sessionDate} (session ${options.sessionNumber})
|
|
425
690
|
|
|
691
|
+
Session lane: ${options.sessionId}
|
|
692
|
+
|
|
426
693
|
## Builder → Reviewer
|
|
427
694
|
|
|
428
695
|
### What changed
|
|
@@ -444,6 +711,53 @@ function renderHandoffTemplate(options) {
|
|
|
444
711
|
`;
|
|
445
712
|
}
|
|
446
713
|
|
|
714
|
+
function renderLaneMetadata(options) {
|
|
715
|
+
return [
|
|
716
|
+
`id: ${options.sessionId}`,
|
|
717
|
+
'status: active',
|
|
718
|
+
`session_date: ${options.sessionDate}`,
|
|
719
|
+
`session_number: ${options.sessionNumber}`,
|
|
720
|
+
`working_on: ${quoteYamlString(options.workingOn)}`,
|
|
721
|
+
renderYamlArray('feature_slugs', options.features),
|
|
722
|
+
renderYamlArray('repos', options.repos),
|
|
723
|
+
renderYamlArray('claimed_paths', options.claims),
|
|
724
|
+
`started_at: ${new Date().toISOString()}`,
|
|
725
|
+
'decision_snapshot:',
|
|
726
|
+
' highest_decision_id: null',
|
|
727
|
+
'',
|
|
728
|
+
].join('\n');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function renderActiveSessionIndex(current, lanes) {
|
|
732
|
+
const lines = [
|
|
733
|
+
`current: ${current ?? 'null'}`,
|
|
734
|
+
'lanes:',
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
for (const lane of lanes.sort((left, right) => left.id.localeCompare(right.id))) {
|
|
738
|
+
lines.push(` - id: ${lane.id}`);
|
|
739
|
+
lines.push(` status: ${lane.status ?? 'active'}`);
|
|
740
|
+
lines.push(` working_on: ${quoteYamlString(lane.workingOn ?? '')}`);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return `${lines.join('\n')}\n`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function renderYamlArray(key, values) {
|
|
747
|
+
if (!values || values.length === 0) {
|
|
748
|
+
return `${key}: []`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return [
|
|
752
|
+
`${key}:`,
|
|
753
|
+
...values.map((value) => ` - ${quoteYamlString(value)}`),
|
|
754
|
+
].join('\n');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function quoteYamlString(value) {
|
|
758
|
+
return JSON.stringify(value);
|
|
759
|
+
}
|
|
760
|
+
|
|
447
761
|
function renderSessionNote(options) {
|
|
448
762
|
return `# Session — ${options.sessionDate}-${options.sessionNumber} — ${options.title}
|
|
449
763
|
|
package/src/workflow.js
CHANGED
|
@@ -77,7 +77,7 @@ export function buildWorkflowMetadata(existingMetadata, overrides = {}) {
|
|
|
77
77
|
export function buildCloseSessionGuidance(workflowSettings) {
|
|
78
78
|
const guidance = [];
|
|
79
79
|
|
|
80
|
-
guidance.push('Reviewer handback stays in sessions/wip.md and
|
|
80
|
+
guidance.push('Reviewer handback stays in the selected sessions/active/<lane-id>/wip.md and handoff.md before close-session runs.');
|
|
81
81
|
|
|
82
82
|
if (workflowSettings.closeSession.refreshArchitectureDocs) {
|
|
83
83
|
guidance.push('Refresh any relevant architecture docs before finalizing the session.');
|