@vibecompass/vibecompass 0.2.0 → 0.2.1
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 +6 -6
- package/package.json +1 -1
- package/src/cli.js +12 -2
- package/src/generators/agent-files/template.js +1 -1
- package/src/scaffold.js +2 -2
- package/src/session.js +76 -19
- package/src/setup.js +54 -0
package/README.md
CHANGED
|
@@ -28,8 +28,6 @@ Not shipped yet:
|
|
|
28
28
|
|
|
29
29
|
The package is publishable now as the file-contract and read-model core. The
|
|
30
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.
|
|
33
31
|
|
|
34
32
|
## Requirements
|
|
35
33
|
|
|
@@ -84,6 +82,7 @@ npx -y @vibecompass/vibecompass init \
|
|
|
84
82
|
--close-session-git-publish \
|
|
85
83
|
--close-session-git-remote origin \
|
|
86
84
|
--start-session \
|
|
85
|
+
--session-id mcp-dogfood \
|
|
87
86
|
--session-working-on "Validate the local-first MCP workflow"
|
|
88
87
|
```
|
|
89
88
|
|
|
@@ -143,8 +142,8 @@ the builder closes the session with `vibecompass close-session --session
|
|
|
143
142
|
|
|
144
143
|
Active builder sessions are named lanes under
|
|
145
144
|
`sessions/active/<lane-id>/`. Use one lane per active feature or workstream.
|
|
146
|
-
`start-session`
|
|
147
|
-
|
|
145
|
+
`start-session` requires `--id <lane-id>` for every lane, including the first
|
|
146
|
+
one in a project.
|
|
148
147
|
Finalized sessions are append-only notes named
|
|
149
148
|
`sessions/YYYY-MM-DD-N-title.md`, so multiple sessions on the same day
|
|
150
149
|
increment `N`. Decisions remain append-only in `decisions/`; a session note
|
|
@@ -234,6 +233,7 @@ Options:
|
|
|
234
233
|
- `--with-agents`: create a starter `AGENTS.md` if it does not already exist
|
|
235
234
|
- `--start-session`: open the first builder session after init
|
|
236
235
|
- `--session-working-on <text>`: required with `--start-session` outside guided mode
|
|
236
|
+
- `--session-id <lane-id>`: required with `--start-session`; names the first builder lane
|
|
237
237
|
- `--close-session-git-publish`: store that the close-session workflow includes a Git publish step
|
|
238
238
|
- `--close-session-git-remote <name>`: optional default Git remote name for that stored publish step
|
|
239
239
|
- `--force`: overwrite an existing `project.yaml`
|
|
@@ -247,7 +247,7 @@ include a Git publish step and which remote name to use for that step.
|
|
|
247
247
|
### `vibecompass start-session`
|
|
248
248
|
|
|
249
249
|
```text
|
|
250
|
-
vibecompass start-session --working-on <text> [options]
|
|
250
|
+
vibecompass start-session --id <lane-id> --working-on <text> [options]
|
|
251
251
|
```
|
|
252
252
|
|
|
253
253
|
Options:
|
|
@@ -255,7 +255,7 @@ Options:
|
|
|
255
255
|
- `--root <path>`: project-memory root; defaults to `.compass`
|
|
256
256
|
- `--tooling-root <path>`: workspace root that contains `CLAUDE.md`; defaults to cwd
|
|
257
257
|
- `--working-on <text>`: required active-session summary
|
|
258
|
-
- `--id <lane-id>`:
|
|
258
|
+
- `--id <lane-id>`: required lane ID
|
|
259
259
|
- `--feature <slug>`: repeatable feature slug for the lane
|
|
260
260
|
- `--repo <id>`: repeatable repo ID for the lane
|
|
261
261
|
- `--claim <path>`: repeatable path claim for overlap warnings
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -62,6 +62,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
62
62
|
const sessionResult = await startProjectSession({
|
|
63
63
|
rootDir: initPlan.initOptions.rootDir,
|
|
64
64
|
toolingRootDir: initPlan.initOptions.toolingRootDir,
|
|
65
|
+
sessionId: initPlan.sessionPlan.sessionId,
|
|
65
66
|
workingOn: initPlan.sessionPlan.workingOn,
|
|
66
67
|
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
67
68
|
});
|
|
@@ -69,6 +70,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
69
70
|
io.stdout.write(`Updated ${sessionResult.claudePath}\n`);
|
|
70
71
|
io.stdout.write(`Created ${sessionResult.wipFilePath}\n`);
|
|
71
72
|
io.stdout.write(`Created ${sessionResult.handoffFilePath}\n`);
|
|
73
|
+
writeAgentFileSyncResult(io, sessionResult.agentFileSync);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
return 0;
|
|
@@ -83,6 +85,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
83
85
|
io.stdout.write(`Updated ${result.claudePath}\n`);
|
|
84
86
|
io.stdout.write(`Created ${result.wipFilePath}\n`);
|
|
85
87
|
io.stdout.write(`Created ${result.handoffFilePath}\n`);
|
|
88
|
+
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
86
89
|
return 0;
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -127,6 +130,8 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
127
130
|
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
128
131
|
});
|
|
129
132
|
io.stdout.write(`Current session lane: ${result.current}\n`);
|
|
133
|
+
io.stdout.write(`Updated ${result.claudePath}\n`);
|
|
134
|
+
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
130
135
|
return 0;
|
|
131
136
|
}
|
|
132
137
|
|
|
@@ -270,6 +275,9 @@ export function parseCliArgs(argv) {
|
|
|
270
275
|
case '--session-working-on':
|
|
271
276
|
parsed.sessionWorkingOn = value;
|
|
272
277
|
break;
|
|
278
|
+
case '--session-id':
|
|
279
|
+
parsed.sessionId = value;
|
|
280
|
+
break;
|
|
273
281
|
case '--close-session-git-remote':
|
|
274
282
|
parsed.closeSessionGitRemote = value;
|
|
275
283
|
break;
|
|
@@ -306,6 +314,7 @@ export function parseCliArgs(argv) {
|
|
|
306
314
|
force: parsed.force,
|
|
307
315
|
startSession: parsed.startSession,
|
|
308
316
|
sessionWorkingOn: parsed.sessionWorkingOn,
|
|
317
|
+
sessionId: parsed.sessionId,
|
|
309
318
|
closeSessionGitPublish: parsed.closeSessionGitPublish,
|
|
310
319
|
closeSessionGitRemote: parsed.closeSessionGitRemote,
|
|
311
320
|
...(parsed.withWorkflow || parsed.withClaude || parsed.withAgents
|
|
@@ -604,7 +613,7 @@ function usageText() {
|
|
|
604
613
|
return [
|
|
605
614
|
'Usage:',
|
|
606
615
|
' vibecompass init --name <project-name> --mode <local-only|local-primary|hosted-only> --repo <id=remote> [options]',
|
|
607
|
-
' vibecompass start-session --working-on <text> [options]',
|
|
616
|
+
' vibecompass start-session --id <lane-id> --working-on <text> [options]',
|
|
608
617
|
' vibecompass close-session --title <text> --completed <text> --next-step <text> [options]',
|
|
609
618
|
' vibecompass end-session --title <text> --completed <text> --next-step <text> [options] # alias',
|
|
610
619
|
' vibecompass list-sessions [options]',
|
|
@@ -629,6 +638,7 @@ function usageText() {
|
|
|
629
638
|
' --with-agents Create a starter AGENTS.md if missing',
|
|
630
639
|
' --start-session Open the first builder session after init',
|
|
631
640
|
' --session-working-on <text> Required with --start-session outside guided mode',
|
|
641
|
+
' --session-id <lane-id> Required with --start-session; names the first builder lane',
|
|
632
642
|
' --close-session-git-publish Include a Git publish step in the stored close-session workflow',
|
|
633
643
|
' --close-session-git-remote <name> Default Git remote name for that close-session publish step',
|
|
634
644
|
' --force Overwrite an existing project.yaml',
|
|
@@ -637,7 +647,7 @@ function usageText() {
|
|
|
637
647
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
638
648
|
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
|
|
639
649
|
' --working-on <text> Required active-session summary',
|
|
640
|
-
' --id <lane-id>
|
|
650
|
+
' --id <lane-id> Required active session lane ID',
|
|
641
651
|
' --feature <slug> Repeatable feature slug for the lane',
|
|
642
652
|
' --repo <id> Repeatable repo ID for the lane',
|
|
643
653
|
' --claim <path> Repeatable path claim for overlap warnings',
|
|
@@ -36,7 +36,7 @@ export function renderSharedInstructionBody(context, options = {}) {
|
|
|
36
36
|
'## Workflow Rules',
|
|
37
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`.',
|
|
38
38
|
'- Use `start session` for builder work and `join as reviewer` for review work.',
|
|
39
|
-
'- Use `vibecompass start-session --id <lane-id>`
|
|
39
|
+
'- Use `vibecompass start-session --id <lane-id>` to open a named lane for every active feature or workstream.',
|
|
40
40
|
'- Use `vibecompass list-sessions` and `vibecompass switch-session <lane-id>` to inspect or change the current lane.',
|
|
41
41
|
'- Treat `sessions/active/index.yaml` as the current lane source of truth; tool-specific Current session blocks are continuity summaries.',
|
|
42
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.',
|
package/src/scaffold.js
CHANGED
|
@@ -125,7 +125,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
125
125
|
|
|
126
126
|
## Session model
|
|
127
127
|
- VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream.
|
|
128
|
-
- \`vibecompass start-session\`
|
|
128
|
+
- \`vibecompass start-session\` requires \`--id <lane-id>\` so each active lane has a meaningful feature or workstream name.
|
|
129
129
|
- The active lane scratch files live under \`${rootRelativePath}/sessions/active/<lane-id>/\`.
|
|
130
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
131
|
- Finalized sessions are append-only notes named \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; multiple sessions on the same day increment \`N\`.
|
|
@@ -163,7 +163,7 @@ ${renderWorkflowDefaults(workflow)}
|
|
|
163
163
|
6. Read the relevant docs under \`${rootRelativePath}/architecture/\` and \`${rootRelativePath}/decisions/\`.
|
|
164
164
|
|
|
165
165
|
## Builder workflow
|
|
166
|
-
At session start, prefer running \`vibecompass start-session --id <lane-id> --working-on "..." \`.
|
|
166
|
+
At session start, prefer running \`vibecompass start-session --id <lane-id> --working-on "..." \`.
|
|
167
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:
|
|
168
168
|
|
|
169
169
|
\`\`\`md
|
package/src/session.js
CHANGED
|
@@ -99,6 +99,10 @@ export async function startProjectSession(options) {
|
|
|
99
99
|
status: 'active',
|
|
100
100
|
workingOn,
|
|
101
101
|
}, { current: sessionId });
|
|
102
|
+
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
103
|
+
rootDir: normalized.rootDir,
|
|
104
|
+
toolingRootDir: normalized.toolingRootDir,
|
|
105
|
+
});
|
|
102
106
|
|
|
103
107
|
return {
|
|
104
108
|
rootDir: normalized.rootDir,
|
|
@@ -110,6 +114,7 @@ export async function startProjectSession(options) {
|
|
|
110
114
|
handoffFilePath: lanePaths.handoffFilePath,
|
|
111
115
|
sessionDate,
|
|
112
116
|
sessionNumber: nextSessionNumber,
|
|
117
|
+
agentFileSync,
|
|
113
118
|
};
|
|
114
119
|
}
|
|
115
120
|
|
|
@@ -185,9 +190,10 @@ export async function closeProjectSession(options) {
|
|
|
185
190
|
);
|
|
186
191
|
|
|
187
192
|
const hadHandoff = await fileExists(lanePaths.handoffFilePath);
|
|
193
|
+
let activeSessionIndex = null;
|
|
188
194
|
if (sessionId) {
|
|
189
195
|
await rm(lanePaths.laneDir, { recursive: true, force: true });
|
|
190
|
-
await removeActiveSessionFromIndex(normalized, sessionId);
|
|
196
|
+
activeSessionIndex = await removeActiveSessionFromIndex(normalized, sessionId);
|
|
191
197
|
} else {
|
|
192
198
|
await rm(lanePaths.wipFilePath, { force: true });
|
|
193
199
|
if (hadHandoff) {
|
|
@@ -195,16 +201,22 @@ export async function closeProjectSession(options) {
|
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
const closedSummary = `Closed session ${activeSession.sessionNumber} and wrote \`${sessionRelativePath}\`.`;
|
|
205
|
+
const updatedClaude = replaceCurrentSessionBlock(
|
|
206
|
+
claude.content,
|
|
207
|
+
activeSessionIndex?.current
|
|
208
|
+
? buildCurrentSessionFieldsForLane(activeSessionIndex.lanes.find((lane) => lane.id === activeSessionIndex.current), {
|
|
209
|
+
lastThingCompleted: normalizeOptionalString(options?.lastThingCompleted) ?? closedSummary,
|
|
210
|
+
})
|
|
211
|
+
: {
|
|
212
|
+
date: `${activeSession.sessionDate} (session ${activeSession.sessionNumber})`,
|
|
213
|
+
workingOn: 'Session closed. Ready for the next builder session.',
|
|
214
|
+
lastThingCompleted: normalizeOptionalString(options?.lastThingCompleted) ?? closedSummary,
|
|
215
|
+
blockers: blockers.length > 0 ? summarizeList(blockers) : 'No blocker remains.',
|
|
216
|
+
nextSessionShould:
|
|
217
|
+
normalizeOptionalString(options?.nextSessionShould) ?? summarizeOrderedList(nextSteps),
|
|
218
|
+
},
|
|
219
|
+
);
|
|
208
220
|
|
|
209
221
|
await writeFile(normalized.claudePath, updatedClaude, 'utf8');
|
|
210
222
|
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
@@ -248,17 +260,35 @@ export async function switchProjectSession(options = {}) {
|
|
|
248
260
|
const normalized = normalizeSessionPaths(options);
|
|
249
261
|
const sessionId = validateLaneId(requireNonEmptyString(options?.sessionId, 'switch-session requires a session ID.'));
|
|
250
262
|
const lanes = await listActiveSessionLanes(normalized);
|
|
263
|
+
const lane = lanes.find((item) => item.id === sessionId);
|
|
251
264
|
|
|
252
|
-
if (!
|
|
265
|
+
if (!lane) {
|
|
253
266
|
throw new Error(`Active session lane "${sessionId}" does not exist.`);
|
|
254
267
|
}
|
|
255
268
|
|
|
256
269
|
await upsertActiveSessionIndex(normalized, null, { current: sessionId });
|
|
270
|
+
const claude = await readClaudeFile(normalized.claudePath);
|
|
271
|
+
await writeFile(
|
|
272
|
+
normalized.claudePath,
|
|
273
|
+
replaceCurrentSessionBlock(
|
|
274
|
+
claude.content,
|
|
275
|
+
buildCurrentSessionFieldsForLane(lane, {
|
|
276
|
+
lastThingCompleted: `Switched current lane to ${sessionId}.`,
|
|
277
|
+
}),
|
|
278
|
+
),
|
|
279
|
+
'utf8',
|
|
280
|
+
);
|
|
281
|
+
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
282
|
+
rootDir: normalized.rootDir,
|
|
283
|
+
toolingRootDir: normalized.toolingRootDir,
|
|
284
|
+
});
|
|
257
285
|
|
|
258
286
|
return {
|
|
259
287
|
rootDir: normalized.rootDir,
|
|
288
|
+
claudePath: normalized.claudePath,
|
|
260
289
|
current: sessionId,
|
|
261
290
|
lanes,
|
|
291
|
+
agentFileSync,
|
|
262
292
|
};
|
|
263
293
|
}
|
|
264
294
|
|
|
@@ -397,6 +427,26 @@ function replaceCurrentSessionBlock(content, fields) {
|
|
|
397
427
|
return `${content.slice(0, sessionFence.bodyStart)}${blockBody}${content.slice(sessionFence.bodyEnd)}`;
|
|
398
428
|
}
|
|
399
429
|
|
|
430
|
+
function buildCurrentSessionFieldsForLane(lane, overrides = {}) {
|
|
431
|
+
if (!lane) {
|
|
432
|
+
throw new Error('Cannot update the Current session block without active lane metadata.');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const sessionDate = lane.sessionDate ?? normalizeSessionDate();
|
|
436
|
+
const sessionNumber = lane.sessionNumber || 0;
|
|
437
|
+
const workingOn = lane.workingOn ?? 'No summary recorded.';
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
date: `${sessionDate} (session ${sessionNumber}, lane ${lane.id})`,
|
|
441
|
+
workingOn: `${workingOn} [${lane.id}]`,
|
|
442
|
+
lastThingCompleted:
|
|
443
|
+
overrides.lastThingCompleted ?? 'Current lane selected from `sessions/active/index.yaml`.',
|
|
444
|
+
blockers: overrides.blockers ?? 'No blocker recorded for the selected lane.',
|
|
445
|
+
nextSessionShould:
|
|
446
|
+
overrides.nextSessionShould ?? 'Continue the selected active lane from `sessions/active/index.yaml`.',
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
400
450
|
async function getNextSessionNumber(sessionsDir, sessionDate) {
|
|
401
451
|
const sessions = await listFinalizedSessions(sessionsDir);
|
|
402
452
|
const activeSessions = await listActiveSessionLanes({ sessionsDir, activeSessionsDir: path.join(sessionsDir, 'active') });
|
|
@@ -414,18 +464,13 @@ async function resolveStartSessionId(normalized, options) {
|
|
|
414
464
|
return validateLaneId(explicitId);
|
|
415
465
|
}
|
|
416
466
|
|
|
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
467
|
if ((await fileExists(normalized.wipFilePath)) || (await fileExists(normalized.handoffFilePath))) {
|
|
423
468
|
throw new Error(
|
|
424
469
|
`Legacy active session scratch files already exist under ${normalized.sessionsDir}. Close or recover that session before starting a lane.`,
|
|
425
470
|
);
|
|
426
471
|
}
|
|
427
472
|
|
|
428
|
-
|
|
473
|
+
throw new Error('start-session requires --id <lane-id> so each active lane has a meaningful name.');
|
|
429
474
|
}
|
|
430
475
|
|
|
431
476
|
async function resolveExistingSessionId(normalized, options) {
|
|
@@ -547,16 +592,28 @@ async function upsertActiveSessionIndex(normalized, lane, options = {}) {
|
|
|
547
592
|
await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, [...laneMap.values()]), 'utf8');
|
|
548
593
|
}
|
|
549
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Removes a closed lane from the active index and returns the surviving lane
|
|
597
|
+
* state used to refresh the Current session block. `lanes` contains the full
|
|
598
|
+
* lane metadata read from each sibling lane's `session.yaml` / `wip.md`.
|
|
599
|
+
*/
|
|
550
600
|
async function removeActiveSessionFromIndex(normalized, sessionId) {
|
|
551
601
|
const index = await readActiveSessionIndex(normalized);
|
|
552
602
|
const lanes = (await listActiveSessionLanes(normalized)).filter((lane) => lane.id !== sessionId);
|
|
553
603
|
const current = index.current === sessionId ? lanes[0]?.id ?? null : index.current;
|
|
554
604
|
if (lanes.length === 0) {
|
|
555
605
|
await rm(normalized.activeSessionsIndexPath, { force: true });
|
|
556
|
-
return
|
|
606
|
+
return {
|
|
607
|
+
current: null,
|
|
608
|
+
lanes,
|
|
609
|
+
};
|
|
557
610
|
}
|
|
558
611
|
|
|
559
612
|
await writeFile(normalized.activeSessionsIndexPath, renderActiveSessionIndex(current, lanes), 'utf8');
|
|
613
|
+
return {
|
|
614
|
+
current,
|
|
615
|
+
lanes,
|
|
616
|
+
};
|
|
560
617
|
}
|
|
561
618
|
|
|
562
619
|
async function listFinalizedSessions(sessionsDir) {
|
package/src/setup.js
CHANGED
|
@@ -40,6 +40,10 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
40
40
|
throw new Error('init requires --session-working-on when --start-session is used without guided setup.');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
if (resolved.startSession && !resolved.sessionId) {
|
|
44
|
+
throw new Error('init requires --session-id when --start-session is used.');
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
return {
|
|
44
48
|
initOptions: {
|
|
45
49
|
rootDir: resolved.rootDir,
|
|
@@ -62,6 +66,7 @@ export async function resolveInitCliOptions(options, environment = {}) {
|
|
|
62
66
|
guidedSummary,
|
|
63
67
|
sessionPlan: resolved.startSession
|
|
64
68
|
? {
|
|
69
|
+
sessionId: resolved.sessionId,
|
|
65
70
|
workingOn: resolved.sessionWorkingOn,
|
|
66
71
|
}
|
|
67
72
|
: null,
|
|
@@ -278,6 +283,15 @@ async function completeGuidedInitOptions(options, environment) {
|
|
|
278
283
|
if (!resolved.sessionWorkingOn) {
|
|
279
284
|
resolved.sessionWorkingOn = await askInput(prompter, 'What are you working on?');
|
|
280
285
|
}
|
|
286
|
+
|
|
287
|
+
if (!resolved.sessionId) {
|
|
288
|
+
resolved.sessionId = await askInput(prompter, 'Session lane id', {
|
|
289
|
+
defaultValue: suggestLaneId(resolved.sessionWorkingOn),
|
|
290
|
+
validate(value) {
|
|
291
|
+
return validateLaneIdForPrompt(value);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
281
295
|
}
|
|
282
296
|
|
|
283
297
|
if (resolved.closeSessionGitPublish === undefined && (resolved.bootstrap.workflow || resolved.startSession)) {
|
|
@@ -669,6 +683,46 @@ function suggestRepoId(value) {
|
|
|
669
683
|
return slug || 'repo';
|
|
670
684
|
}
|
|
671
685
|
|
|
686
|
+
function suggestLaneId(value) {
|
|
687
|
+
const slug = String(value ?? '')
|
|
688
|
+
.toLowerCase()
|
|
689
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
690
|
+
.replace(/^-+|-+$/g, '')
|
|
691
|
+
.slice(0, 63)
|
|
692
|
+
.replace(/-+$/g, '');
|
|
693
|
+
|
|
694
|
+
return slug.length >= 3 ? slug : 'builder-session';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function validateLaneIdForPrompt(value) {
|
|
698
|
+
if (!/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(value)) {
|
|
699
|
+
return 'Lane id must be 3-64 lowercase letters, numbers, or hyphens, and must start and end with a letter or number.';
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if ([
|
|
703
|
+
'active',
|
|
704
|
+
'current',
|
|
705
|
+
'default',
|
|
706
|
+
'false',
|
|
707
|
+
'handoff',
|
|
708
|
+
'index',
|
|
709
|
+
'new',
|
|
710
|
+
'no',
|
|
711
|
+
'null',
|
|
712
|
+
'off',
|
|
713
|
+
'on',
|
|
714
|
+
'sessions',
|
|
715
|
+
'state',
|
|
716
|
+
'true',
|
|
717
|
+
'wip',
|
|
718
|
+
'yes',
|
|
719
|
+
].includes(value)) {
|
|
720
|
+
return `Lane id "${value}" is reserved.`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
672
726
|
function getRepoDirectoryHints(repo) {
|
|
673
727
|
const hints = new Set();
|
|
674
728
|
|