@vibecompass/vibecompass 0.10.7 → 0.11.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 +37 -1
- package/README.md +42 -1
- package/package.json +1 -1
- package/src/cli.js +602 -8
- package/src/compatibility.js +34 -1
- package/src/decision-index.js +386 -0
- package/src/decisions.js +195 -0
- package/src/docs-review.js +27 -27
- package/src/docs-update.js +459 -24
- package/src/generators/agent-files/index.js +8 -0
- package/src/generators/agent-files/template.js +6 -3
- package/src/git-binding.js +676 -0
- package/src/init.js +9 -0
- package/src/lane-marker.js +339 -0
- package/src/lane-runtime.js +254 -0
- package/src/manifest.js +19 -0
- package/src/project-memory.js +27 -1
- package/src/refresh-workflow.js +8 -0
- package/src/scaffold.js +24 -12
- package/src/serialization.js +205 -0
- package/src/session.js +955 -80
- package/src/simple-yaml.js +24 -4
- package/src/status.js +43 -5
- package/src/sync.js +5 -0
- package/src/workflow.js +39 -1
- package/src/workflows/registry.js +7 -4
package/src/cli.js
CHANGED
|
@@ -6,7 +6,12 @@ import { connectHostedProjectMemory, initializeProjectMemory, setDefaultSyncTarg
|
|
|
6
6
|
import { preflightDocsReview } from './docs-review.js';
|
|
7
7
|
import { planDocsUpdate, renderDocsUpdatePlan } from './docs-update.js';
|
|
8
8
|
import { resolveConnectHostedCliOptions, resolveInitCliOptions } from './setup.js';
|
|
9
|
-
import {
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { closeProjectSession, listProjectSessions, readLaneEnvironment, rebuildActiveSessionIndex, startProjectSession, switchProjectSession, writeLaneMarkerForSession } from './session.js';
|
|
12
|
+
import { appendDecisionEntry, formatDecisionId, readNextDecisionId } from './decisions.js';
|
|
13
|
+
import { checkGroupedDecisionIndex, refreshGroupedDecisionIndex, resolveDefaultIndexGroupLabel } from './decision-index.js';
|
|
14
|
+
import { resolveLaneMarkerContext, resolveLaneSelection } from './lane-marker.js';
|
|
10
15
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
11
16
|
import { getProjectStatus, renderStatusText, toStatusJson } from './status.js';
|
|
12
17
|
import { refreshWorkflow } from './refresh-workflow.js';
|
|
@@ -130,11 +135,52 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
130
135
|
io.stdout.write(`Updated ${result.claudePath}\n`);
|
|
131
136
|
io.stdout.write(`Created ${result.wipFilePath}\n`);
|
|
132
137
|
io.stdout.write(`Created ${result.handoffFilePath}\n`);
|
|
138
|
+
if (result.gitBinding) {
|
|
139
|
+
io.stdout.write(`Git binding: branch "${result.gitBinding.branch}"\n`);
|
|
140
|
+
for (const repo of result.gitBinding.repos) {
|
|
141
|
+
const location = repo.worktreePath ? ` at ${repo.worktreePath}` : '';
|
|
142
|
+
io.stdout.write(`- ${repo.repoId}: ${repo.mode === 'reuse' ? 'reused existing branch' : 'created branch'}${location} (base ${repo.baseRevision.slice(0, 12)})\n`);
|
|
143
|
+
}
|
|
144
|
+
if (result.gitBinding.markerPath) {
|
|
145
|
+
io.stdout.write(`Lane marker: ${result.gitBinding.markerPath}\n`);
|
|
146
|
+
const firstWorktree = result.gitBinding.repos.find((repo) => repo.worktreePath);
|
|
147
|
+
if (firstWorktree) {
|
|
148
|
+
io.stdout.write(`Next: cd ${firstWorktree.worktreePath} — commands run from inside a worktree need no --root or --session (lane marker).\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (result.runtime) {
|
|
153
|
+
io.stdout.write(`Runtime: port ${result.runtime.port}, temp dir ${result.runtime.tmpDir}\n`);
|
|
154
|
+
io.stdout.write('Export into a shell with: eval "$(vibecompass lane-env)" (D-282; pass --session <lane-id> when multiple lanes are active).\n');
|
|
155
|
+
}
|
|
133
156
|
writeWarnings(io, result.warnings);
|
|
134
157
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
135
158
|
return 0;
|
|
136
159
|
}
|
|
137
160
|
|
|
161
|
+
if (parsed.command === 'lane-env') {
|
|
162
|
+
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
163
|
+
const result = await readLaneEnvironment({
|
|
164
|
+
...parsed.options,
|
|
165
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
166
|
+
});
|
|
167
|
+
if (parsed.options.json) {
|
|
168
|
+
io.stdout.write(`${JSON.stringify({
|
|
169
|
+
root_dir: result.rootDir,
|
|
170
|
+
lane_id: result.sessionId,
|
|
171
|
+
port: result.port,
|
|
172
|
+
tmp_dir: result.tmpDir,
|
|
173
|
+
env: result.env,
|
|
174
|
+
}, null, 2)}\n`);
|
|
175
|
+
} else {
|
|
176
|
+
for (const [name, value] of Object.entries(result.env)) {
|
|
177
|
+
io.stdout.write(`export ${name}=${shellQuoteSingle(value)}\n`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
writeWarnings(io, result.warnings);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
138
184
|
if (parsed.command === 'connect-hosted') {
|
|
139
185
|
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
140
186
|
const connectPlan = await resolveConnectHostedCliOptions(parsed.options, {
|
|
@@ -232,6 +278,8 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
232
278
|
io.stdout.write(`Closed session ${result.sessionDate}-${result.sessionNumber}\n`);
|
|
233
279
|
io.stdout.write(`Wrote ${result.sessionFilePath}\n`);
|
|
234
280
|
io.stdout.write(`Updated ${result.claudePath}\n`);
|
|
281
|
+
writeWorktreeCleanupResult(io, result.worktreeCleanup);
|
|
282
|
+
writeRuntimeCleanupResult(io, result.runtimeCleanup);
|
|
235
283
|
if (result.docsUpdatePlan) {
|
|
236
284
|
io.stdout.write(renderDocsUpdatePlan(result.docsUpdatePlan));
|
|
237
285
|
}
|
|
@@ -446,6 +494,133 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
446
494
|
return 0;
|
|
447
495
|
}
|
|
448
496
|
|
|
497
|
+
if (parsed.command === 'rebuild-active-index') {
|
|
498
|
+
const result = await rebuildActiveSessionIndex({
|
|
499
|
+
...parsed.options,
|
|
500
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
501
|
+
});
|
|
502
|
+
if (result.removed) {
|
|
503
|
+
io.stdout.write('No active lanes; removed sessions/active/index.yaml.\n');
|
|
504
|
+
} else {
|
|
505
|
+
io.stdout.write(`Rebuilt ${result.indexPath}\n`);
|
|
506
|
+
io.stdout.write(`Current lane: ${result.current}\n`);
|
|
507
|
+
for (const lane of result.lanes) {
|
|
508
|
+
io.stdout.write(`- ${lane.id}${lane.workingOn ? `: ${lane.workingOn}` : ''}\n`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (parsed.command === 'write-lane-marker') {
|
|
515
|
+
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
516
|
+
const result = await writeLaneMarkerForSession({
|
|
517
|
+
...parsed.options,
|
|
518
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
519
|
+
});
|
|
520
|
+
io.stdout.write(`Wrote lane marker for "${result.sessionId}" at ${result.markerPath}\n`);
|
|
521
|
+
io.stdout.write(`Recorded the marker in the lane's session.yaml (token-matched removal at close-session).\n`);
|
|
522
|
+
writeWarnings(io, result.warnings);
|
|
523
|
+
return 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (parsed.command === 'append-decision') {
|
|
527
|
+
const cwd = runtime.cwd ? path.resolve(runtime.cwd) : process.cwd();
|
|
528
|
+
if (!parsed.options.entryPath) {
|
|
529
|
+
throw new Error('append-decision requires --entry <staged-entry-file>.');
|
|
530
|
+
}
|
|
531
|
+
// D-280 marker-aware root: an explicit --root wins, else the nearest
|
|
532
|
+
// worktree lane marker supplies the root, so a builder appending a
|
|
533
|
+
// decision mid-lane from inside a worktree needs neither --root nor
|
|
534
|
+
// --session (matching lane-env / close-session).
|
|
535
|
+
const markerContext = await resolveDecisionCommandContext(cwd, parsed.options);
|
|
536
|
+
const rootDir = markerContext.rootDir;
|
|
537
|
+
const entryContent = await readFile(path.resolve(cwd, parsed.options.entryPath), 'utf8');
|
|
538
|
+
// D-283: refresh the grouped index when a group label is determinable —
|
|
539
|
+
// explicit --group wins, else the resolvable lane context labels the
|
|
540
|
+
// group. With neither, the append keeps the hand-refresh reminder.
|
|
541
|
+
const labelResult = parsed.options.groupLabel
|
|
542
|
+
? { label: parsed.options.groupLabel, warnings: [] }
|
|
543
|
+
: await resolveIndexGroupLabelSafely({ rootDir, cwd, sessionId: parsed.options.sessionId ?? null, marker: markerContext.marker });
|
|
544
|
+
const groupLabel = labelResult.label;
|
|
545
|
+
const result = await appendDecisionEntry({
|
|
546
|
+
rootDir,
|
|
547
|
+
target: parsed.options.target,
|
|
548
|
+
entryContent,
|
|
549
|
+
...(groupLabel
|
|
550
|
+
? { indexRefresher: () => refreshGroupedDecisionIndex({ rootDir, groupLabel }) }
|
|
551
|
+
: {}),
|
|
552
|
+
});
|
|
553
|
+
io.stdout.write(`Appended ${formatDecisionId(result.decisionId)} to ${result.targetPath}\n`);
|
|
554
|
+
for (const warning of uniqueStrings([...markerContext.warnings, ...labelResult.warnings, ...result.warnings])) {
|
|
555
|
+
io.stdout.write(`Warning: ${warning}\n`);
|
|
556
|
+
}
|
|
557
|
+
io.stdout.write(`${result.indexReminder}\n`);
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (parsed.command === 'refresh-decision-index') {
|
|
562
|
+
const cwd = runtime.cwd ? path.resolve(runtime.cwd) : process.cwd();
|
|
563
|
+
// D-280 marker-aware root, matching lane-env / close-session, so the
|
|
564
|
+
// command works from inside a provisioned worktree with no flags.
|
|
565
|
+
const markerContext = await resolveDecisionCommandContext(cwd, parsed.options);
|
|
566
|
+
const rootDir = markerContext.rootDir;
|
|
567
|
+
|
|
568
|
+
if (parsed.options.check) {
|
|
569
|
+
const result = await checkGroupedDecisionIndex({ rootDir });
|
|
570
|
+
writeWarnings(io, markerContext.warnings);
|
|
571
|
+
if (result.ok) {
|
|
572
|
+
io.stdout.write(
|
|
573
|
+
`Grouped decision index check: clean (${result.stats.rowCount} rows in ${result.stats.groupCount} groups; ${result.stats.decisionCount} canonical decisions)\n`,
|
|
574
|
+
);
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
io.stderr.write(`Grouped decision index check failed for ${result.indexPath}:\n`);
|
|
578
|
+
for (const problem of result.problems) {
|
|
579
|
+
io.stderr.write(`- ${problem}\n`);
|
|
580
|
+
}
|
|
581
|
+
return 1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Resolve the group label tolerantly: a no-op refresh (nothing missing)
|
|
585
|
+
// must not fail just because 2+ lanes are active with no marker/--session.
|
|
586
|
+
// The label is only load-bearing when rows are actually missing, and
|
|
587
|
+
// refreshGroupedDecisionIndex raises the actionable "pass --group" problem
|
|
588
|
+
// in exactly that case.
|
|
589
|
+
const labelResult = parsed.options.groupLabel
|
|
590
|
+
? { label: parsed.options.groupLabel, warnings: [] }
|
|
591
|
+
: await resolveIndexGroupLabelSafely({ rootDir, cwd, sessionId: parsed.options.sessionId ?? null, marker: markerContext.marker });
|
|
592
|
+
const result = await refreshGroupedDecisionIndex({ rootDir, groupLabel: labelResult.label });
|
|
593
|
+
writeWarnings(io, uniqueStrings([...markerContext.warnings, ...labelResult.warnings, ...result.warnings]));
|
|
594
|
+
if (result.problems.length > 0) {
|
|
595
|
+
io.stderr.write(`Grouped decision index refresh refused for ${result.indexPath} (D-283 fail-closed):\n`);
|
|
596
|
+
for (const problem of result.problems) {
|
|
597
|
+
io.stderr.write(`- ${problem}\n`);
|
|
598
|
+
}
|
|
599
|
+
return 1;
|
|
600
|
+
}
|
|
601
|
+
if (result.upToDate) {
|
|
602
|
+
io.stdout.write(`decisions/INDEX.md is up to date (nothing missing).\n`);
|
|
603
|
+
} else {
|
|
604
|
+
io.stdout.write(`Refreshed ${result.indexPath} (structure-preserving, D-283)\n`);
|
|
605
|
+
io.stdout.write(`Added under "## ${result.groupLabel}":\n`);
|
|
606
|
+
for (const decision of result.added) {
|
|
607
|
+
io.stdout.write(`- ${formatDecisionId(decision.id)} | ${decision.title} | ${decision.domain}.md\n`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return 0;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (parsed.command === 'next-decision-id') {
|
|
614
|
+
const cwd = runtime.cwd ? path.resolve(runtime.cwd) : process.cwd();
|
|
615
|
+
const rootDir = path.resolve(cwd, parsed.options.rootDir ?? '.compass');
|
|
616
|
+
const nextId = await readNextDecisionId(rootDir);
|
|
617
|
+
io.stdout.write(`Advisory next decision ID: ${formatDecisionId(nextId)}\n`);
|
|
618
|
+
io.stdout.write(
|
|
619
|
+
'Allocation happens at write time under the memory-root lock (D-276). Use `vibecompass append-decision`, or re-read the log immediately before appending by hand.\n',
|
|
620
|
+
);
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
449
624
|
throw new Error(`Unknown command "${parsed.command}".`);
|
|
450
625
|
}
|
|
451
626
|
|
|
@@ -497,6 +672,30 @@ export function parseCliArgs(argv) {
|
|
|
497
672
|
return parseSwitchSessionArgs(rest);
|
|
498
673
|
}
|
|
499
674
|
|
|
675
|
+
if (command === 'rebuild-active-index') {
|
|
676
|
+
return parseRebuildActiveIndexArgs(rest);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (command === 'write-lane-marker') {
|
|
680
|
+
return parseWriteLaneMarkerArgs(rest);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (command === 'lane-env') {
|
|
684
|
+
return parseLaneEnvArgs(rest);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (command === 'append-decision') {
|
|
688
|
+
return parseAppendDecisionArgs(rest);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (command === 'refresh-decision-index') {
|
|
692
|
+
return parseRefreshDecisionIndexArgs(rest);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (command === 'next-decision-id') {
|
|
696
|
+
return parseNextDecisionIdArgs(rest);
|
|
697
|
+
}
|
|
698
|
+
|
|
500
699
|
if (command === 'sync-agents') {
|
|
501
700
|
return parseSyncAgentsArgs(rest);
|
|
502
701
|
}
|
|
@@ -712,6 +911,16 @@ function parseStartSessionArgs(argv) {
|
|
|
712
911
|
throw new Error(`Unexpected argument "${token}".`);
|
|
713
912
|
}
|
|
714
913
|
|
|
914
|
+
// Boolean flags first (D-281): --worktree takes no value — the container
|
|
915
|
+
// location is fixed to <workspace>/worktrees/<lane-id> per D-279.
|
|
916
|
+
if (token === '--worktree') {
|
|
917
|
+
parsed.worktree = true;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (token.startsWith('--worktree=')) {
|
|
921
|
+
throw new Error('Flag "--worktree" takes no value; the worktree container is fixed to <workspace>/worktrees/<lane-id> (D-279).');
|
|
922
|
+
}
|
|
923
|
+
|
|
715
924
|
const value = argv[index + 1];
|
|
716
925
|
if (value === undefined) {
|
|
717
926
|
throw new Error(`Flag "${token}" requires a value.`);
|
|
@@ -740,6 +949,9 @@ function parseStartSessionArgs(argv) {
|
|
|
740
949
|
case '--claim':
|
|
741
950
|
parsed.claims = [...(parsed.claims ?? []), value];
|
|
742
951
|
break;
|
|
952
|
+
case '--branch':
|
|
953
|
+
parsed.branch = value;
|
|
954
|
+
break;
|
|
743
955
|
case '--date':
|
|
744
956
|
parsed.date = value;
|
|
745
957
|
break;
|
|
@@ -763,6 +975,50 @@ function parseStartSessionArgs(argv) {
|
|
|
763
975
|
};
|
|
764
976
|
}
|
|
765
977
|
|
|
978
|
+
function parseLaneEnvArgs(argv) {
|
|
979
|
+
const parsed = {};
|
|
980
|
+
|
|
981
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
982
|
+
const token = argv[index];
|
|
983
|
+
|
|
984
|
+
if (!token.startsWith('--')) {
|
|
985
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (token === '--json') {
|
|
989
|
+
parsed.json = true;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (token === '--no-conventional') {
|
|
994
|
+
parsed.conventionalAliases = false;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const value = argv[index + 1];
|
|
999
|
+
if (value === undefined) {
|
|
1000
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1001
|
+
}
|
|
1002
|
+
index += 1;
|
|
1003
|
+
|
|
1004
|
+
switch (token) {
|
|
1005
|
+
case '--root':
|
|
1006
|
+
parsed.rootDir = value;
|
|
1007
|
+
break;
|
|
1008
|
+
case '--session':
|
|
1009
|
+
parsed.sessionId = value;
|
|
1010
|
+
break;
|
|
1011
|
+
default:
|
|
1012
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
command: 'lane-env',
|
|
1018
|
+
options: parsed,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
766
1022
|
function parseConnectHostedArgs(argv) {
|
|
767
1023
|
const parsed = {};
|
|
768
1024
|
|
|
@@ -1071,6 +1327,228 @@ function parseListSessionsArgs(argv) {
|
|
|
1071
1327
|
};
|
|
1072
1328
|
}
|
|
1073
1329
|
|
|
1330
|
+
function parseRebuildActiveIndexArgs(argv) {
|
|
1331
|
+
const parsed = {};
|
|
1332
|
+
|
|
1333
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1334
|
+
const token = argv[index];
|
|
1335
|
+
const value = argv[index + 1];
|
|
1336
|
+
if (value === undefined) {
|
|
1337
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1338
|
+
}
|
|
1339
|
+
index += 1;
|
|
1340
|
+
|
|
1341
|
+
switch (token) {
|
|
1342
|
+
case '--root':
|
|
1343
|
+
parsed.rootDir = value;
|
|
1344
|
+
break;
|
|
1345
|
+
case '--current':
|
|
1346
|
+
parsed.current = value;
|
|
1347
|
+
break;
|
|
1348
|
+
default:
|
|
1349
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
return { command: 'rebuild-active-index', options: parsed };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function parseWriteLaneMarkerArgs(argv) {
|
|
1357
|
+
const parsed = {};
|
|
1358
|
+
|
|
1359
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1360
|
+
const token = argv[index];
|
|
1361
|
+
|
|
1362
|
+
if (!token.startsWith('--')) {
|
|
1363
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const value = argv[index + 1];
|
|
1367
|
+
if (value === undefined) {
|
|
1368
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1369
|
+
}
|
|
1370
|
+
index += 1;
|
|
1371
|
+
|
|
1372
|
+
switch (token) {
|
|
1373
|
+
case '--root':
|
|
1374
|
+
parsed.rootDir = value;
|
|
1375
|
+
break;
|
|
1376
|
+
case '--session':
|
|
1377
|
+
parsed.sessionId = value;
|
|
1378
|
+
break;
|
|
1379
|
+
case '--dir':
|
|
1380
|
+
parsed.dir = value;
|
|
1381
|
+
break;
|
|
1382
|
+
default:
|
|
1383
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return { command: 'write-lane-marker', options: parsed };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function parseAppendDecisionArgs(argv) {
|
|
1391
|
+
const parsed = {};
|
|
1392
|
+
|
|
1393
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1394
|
+
const token = argv[index];
|
|
1395
|
+
const value = argv[index + 1];
|
|
1396
|
+
if (value === undefined) {
|
|
1397
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1398
|
+
}
|
|
1399
|
+
index += 1;
|
|
1400
|
+
|
|
1401
|
+
switch (token) {
|
|
1402
|
+
case '--root':
|
|
1403
|
+
parsed.rootDir = value;
|
|
1404
|
+
break;
|
|
1405
|
+
case '--target':
|
|
1406
|
+
parsed.target = value;
|
|
1407
|
+
break;
|
|
1408
|
+
case '--entry':
|
|
1409
|
+
parsed.entryPath = value;
|
|
1410
|
+
break;
|
|
1411
|
+
case '--group':
|
|
1412
|
+
parsed.groupLabel = value;
|
|
1413
|
+
break;
|
|
1414
|
+
case '--session':
|
|
1415
|
+
parsed.sessionId = value;
|
|
1416
|
+
break;
|
|
1417
|
+
default:
|
|
1418
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return { command: 'append-decision', options: parsed };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function parseRefreshDecisionIndexArgs(argv) {
|
|
1426
|
+
const parsed = {};
|
|
1427
|
+
|
|
1428
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1429
|
+
const token = argv[index];
|
|
1430
|
+
|
|
1431
|
+
if (!token.startsWith('--')) {
|
|
1432
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (token === '--check') {
|
|
1436
|
+
parsed.check = true;
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const value = argv[index + 1];
|
|
1441
|
+
if (value === undefined) {
|
|
1442
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1443
|
+
}
|
|
1444
|
+
index += 1;
|
|
1445
|
+
|
|
1446
|
+
switch (token) {
|
|
1447
|
+
case '--root':
|
|
1448
|
+
parsed.rootDir = value;
|
|
1449
|
+
break;
|
|
1450
|
+
case '--session':
|
|
1451
|
+
parsed.sessionId = value;
|
|
1452
|
+
break;
|
|
1453
|
+
case '--group':
|
|
1454
|
+
parsed.groupLabel = value;
|
|
1455
|
+
break;
|
|
1456
|
+
default:
|
|
1457
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return { command: 'refresh-decision-index', options: parsed };
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* D-280 marker-aware root resolution for the decision-log commands
|
|
1466
|
+
* (append-decision, refresh-decision-index): an explicit --root wins, else the
|
|
1467
|
+
* nearest worktree lane marker supplies the memory root, else cwd/.compass.
|
|
1468
|
+
* Returns the resolved root plus any marker warnings.
|
|
1469
|
+
*/
|
|
1470
|
+
async function resolveDecisionCommandContext(cwd, options) {
|
|
1471
|
+
const markerContext = await resolveLaneMarkerContext({
|
|
1472
|
+
cwd,
|
|
1473
|
+
explicitRootDir: options.rootDir ?? null,
|
|
1474
|
+
explicitSessionId: options.sessionId ?? null,
|
|
1475
|
+
});
|
|
1476
|
+
const rootDir = markerContext.rootDir;
|
|
1477
|
+
const warnings = [...markerContext.warnings];
|
|
1478
|
+
|
|
1479
|
+
// D-277/D-280: validate an explicit selection signal up front, before the
|
|
1480
|
+
// command decides whether it needs a group label. A `--session` naming a
|
|
1481
|
+
// non-active lane, or a resolved worktree marker that is stale (names a
|
|
1482
|
+
// non-active lane), must fail the command closed even when an explicit
|
|
1483
|
+
// `--group` skips label resolution and even for read-only `--check`. The
|
|
1484
|
+
// 2+-lane no-selection ambiguity carries no explicit signal and is left for
|
|
1485
|
+
// tolerant label handling (a no-op refresh must still succeed there).
|
|
1486
|
+
if (options.sessionId || markerContext.marker) {
|
|
1487
|
+
const sessions = await listProjectSessions({ rootDir, cwd });
|
|
1488
|
+
const selection = resolveLaneSelection({
|
|
1489
|
+
explicitSessionId: options.sessionId ?? null,
|
|
1490
|
+
marker: markerContext.marker,
|
|
1491
|
+
laneIds: sessions.lanes.map((lane) => lane.id),
|
|
1492
|
+
rootDir,
|
|
1493
|
+
purpose: 'run this decision-log command in',
|
|
1494
|
+
});
|
|
1495
|
+
warnings.push(...selection.warnings);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
return { rootDir, marker: markerContext.marker, warnings };
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* D-283 auto-labeling for the decision-log commands. Tolerance is narrow: only
|
|
1503
|
+
* the "2+ active lanes with no explicit selection" ambiguity degrades to "no
|
|
1504
|
+
* label" (so a no-op refresh still succeeds and an append still lands with the
|
|
1505
|
+
* hand-refresh reminder). An *explicit* selection signal — a `--session` value
|
|
1506
|
+
* or a resolved worktree marker — must still fail closed on a bad selection
|
|
1507
|
+
* (a `--session` typo naming a non-active lane, or a stale marker) per
|
|
1508
|
+
* D-277/D-280; swallowing those would let `append-decision --session <typo>`
|
|
1509
|
+
* silently append a canonical decision against the wrong/no lane. Selection
|
|
1510
|
+
* warnings are returned so they are no longer discarded.
|
|
1511
|
+
*/
|
|
1512
|
+
async function resolveIndexGroupLabelSafely(options) {
|
|
1513
|
+
const hasExplicitSelection = Boolean(options.sessionId) || Boolean(options.marker);
|
|
1514
|
+
if (hasExplicitSelection) {
|
|
1515
|
+
// Let a bad explicit selection propagate and fail the command closed.
|
|
1516
|
+
const resolved = await resolveDefaultIndexGroupLabel(options);
|
|
1517
|
+
return { label: resolved?.label ?? null, warnings: resolved?.warnings ?? [] };
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
try {
|
|
1521
|
+
const resolved = await resolveDefaultIndexGroupLabel(options);
|
|
1522
|
+
return { label: resolved?.label ?? null, warnings: resolved?.warnings ?? [] };
|
|
1523
|
+
} catch {
|
|
1524
|
+
// Only the 2+-lane no-selection ambiguity reaches here.
|
|
1525
|
+
return { label: null, warnings: [] };
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function parseNextDecisionIdArgs(argv) {
|
|
1530
|
+
const parsed = {};
|
|
1531
|
+
|
|
1532
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1533
|
+
const token = argv[index];
|
|
1534
|
+
const value = argv[index + 1];
|
|
1535
|
+
if (value === undefined) {
|
|
1536
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1537
|
+
}
|
|
1538
|
+
index += 1;
|
|
1539
|
+
|
|
1540
|
+
switch (token) {
|
|
1541
|
+
case '--root':
|
|
1542
|
+
parsed.rootDir = value;
|
|
1543
|
+
break;
|
|
1544
|
+
default:
|
|
1545
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return { command: 'next-decision-id', options: parsed };
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1074
1552
|
function parseSwitchSessionArgs(argv) {
|
|
1075
1553
|
const parsed = {};
|
|
1076
1554
|
const positional = [];
|
|
@@ -1545,6 +2023,85 @@ function writeRefreshWorkflowResult(io, result) {
|
|
|
1545
2023
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
1546
2024
|
}
|
|
1547
2025
|
|
|
2026
|
+
// D-281 close-side worktree cleanup summary. Status lines stay on stdout;
|
|
2027
|
+
// the actionable per-worktree guidance travels on result.warnings (stderr).
|
|
2028
|
+
const SURVIVING_WORKTREE_REASON_LABELS = {
|
|
2029
|
+
'container-unverified': 'container marker unverified',
|
|
2030
|
+
'outside-container': 'outside the lane container',
|
|
2031
|
+
'cwd-inside': 'current working directory is inside it',
|
|
2032
|
+
'not-absolute': 'recorded path not absolute',
|
|
2033
|
+
dirty: 'uncommitted changes',
|
|
2034
|
+
'status-unknown': 'cleanliness unknown',
|
|
2035
|
+
'no-source-recorded': 'no recorded source repo',
|
|
2036
|
+
'git-refused': 'git refused the removal',
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
function writeWorktreeCleanupResult(io, worktreeCleanup) {
|
|
2040
|
+
if (!worktreeCleanup) {
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
io.stdout.write('Worktree cleanup:\n');
|
|
2045
|
+
for (const removed of worktreeCleanup.removed) {
|
|
2046
|
+
io.stdout.write(`- ${removed.repoId}: removed ${removed.worktreePath}\n`);
|
|
2047
|
+
}
|
|
2048
|
+
for (const surviving of worktreeCleanup.surviving) {
|
|
2049
|
+
const label = SURVIVING_WORKTREE_REASON_LABELS[surviving.reason] ?? surviving.reason;
|
|
2050
|
+
io.stdout.write(`- ${surviving.repoId}: kept ${surviving.worktreePath} (${label})\n`);
|
|
2051
|
+
}
|
|
2052
|
+
if (worktreeCleanup.markerRemoved) {
|
|
2053
|
+
io.stdout.write('- lane marker removed (token-matched)\n');
|
|
2054
|
+
} else if (worktreeCleanup.markerKept) {
|
|
2055
|
+
io.stdout.write('- lane marker kept while worktrees survive (D-281)\n');
|
|
2056
|
+
}
|
|
2057
|
+
if (worktreeCleanup.containerRemoved) {
|
|
2058
|
+
io.stdout.write('- container removed (empty)\n');
|
|
2059
|
+
}
|
|
2060
|
+
if (worktreeCleanup.branch) {
|
|
2061
|
+
io.stdout.write(`- branch "${worktreeCleanup.branch}" left in place (close-session never deletes branches; D-281)\n`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// D-282 close-side lane temp-dir cleanup summary. A missing recorded dir is
|
|
2066
|
+
// benign crash residue and prints nothing, matching recorded-but-missing
|
|
2067
|
+
// worktrees; guard refusals print the kept path with a short label while the
|
|
2068
|
+
// actionable guidance travels on result.warnings (stderr).
|
|
2069
|
+
const KEPT_LANE_TMP_REASON_LABELS = {
|
|
2070
|
+
'not-absolute': 'recorded path not absolute',
|
|
2071
|
+
'outside-namespace': 'outside the lane temp namespace',
|
|
2072
|
+
'lane-id-mismatch': "does not end in the lane's id",
|
|
2073
|
+
'cwd-inside': 'current working directory is inside it',
|
|
2074
|
+
'remove-failed': 'removal failed',
|
|
2075
|
+
};
|
|
2076
|
+
|
|
2077
|
+
function writeRuntimeCleanupResult(io, runtimeCleanup) {
|
|
2078
|
+
if (!runtimeCleanup) {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
if (runtimeCleanup.removed) {
|
|
2083
|
+
io.stdout.write(`Lane temp dir: removed ${runtimeCleanup.tmpDir}\n`);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (runtimeCleanup.reason === 'missing') {
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
const label = KEPT_LANE_TMP_REASON_LABELS[runtimeCleanup.reason] ?? runtimeCleanup.reason;
|
|
2092
|
+
io.stdout.write(`Lane temp dir: kept ${runtimeCleanup.tmpDir} (${label})\n`);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
function shellQuoteSingle(value) {
|
|
2096
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
/** Preserves order, drops duplicate warning strings (D-283 eager validation
|
|
2100
|
+
* can surface the same selection warning twice). */
|
|
2101
|
+
function uniqueStrings(items) {
|
|
2102
|
+
return [...new Set(items)];
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1548
2105
|
function writeDocumentMaintenanceCheckpoint(io, documentMaintenance) {
|
|
1549
2106
|
if (!documentMaintenance) {
|
|
1550
2107
|
return;
|
|
@@ -1606,11 +2163,17 @@ function usageText() {
|
|
|
1606
2163
|
' vibecompass status [options]',
|
|
1607
2164
|
' vibecompass refresh-workflow [--dry-run|--apply] [options]',
|
|
1608
2165
|
' vibecompass docs-update [--session <lane-id>] [options]',
|
|
1609
|
-
' vibecompass start-session --id <lane-id> --working-on <text> [options]',
|
|
2166
|
+
' vibecompass start-session --id <lane-id> --working-on <text> [--branch <name> [--worktree]] [options]',
|
|
1610
2167
|
' vibecompass close-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options]',
|
|
1611
2168
|
' vibecompass end-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options] # alias',
|
|
1612
2169
|
' vibecompass list-sessions [options]',
|
|
1613
2170
|
' vibecompass switch-session <id> [options]',
|
|
2171
|
+
' vibecompass rebuild-active-index [--current <lane-id>] [options]',
|
|
2172
|
+
' vibecompass write-lane-marker --session <lane-id> [--dir <path>] [options]',
|
|
2173
|
+
' vibecompass lane-env [--session <lane-id>] [--json] [--no-conventional] [options]',
|
|
2174
|
+
' vibecompass append-decision --target <domain.md> --entry <staged-entry.md> [--group <label>] [options]',
|
|
2175
|
+
' vibecompass refresh-decision-index [--check] [--session <lane-id>] [--group <label>] [options]',
|
|
2176
|
+
' vibecompass next-decision-id [options]',
|
|
1614
2177
|
' vibecompass sync-agents [options]',
|
|
1615
2178
|
' vibecompass push [options]',
|
|
1616
2179
|
' vibecompass pull-preview [options]',
|
|
@@ -1672,30 +2235,32 @@ function usageText() {
|
|
|
1672
2235
|
' --allow-downgrade-templates Allow applying templates with a CLI older than the root stamp',
|
|
1673
2236
|
'',
|
|
1674
2237
|
'Docs-update options:',
|
|
1675
|
-
' --root <path> Project-memory root.
|
|
1676
|
-
' --session <lane-id> Active session lane to inspect;
|
|
2238
|
+
' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
|
|
2239
|
+
' --session <lane-id> Active session lane to inspect. Omitted: nearest worktree lane marker, else the single active lane; 2+ active lanes require --session or a marker (D-277)',
|
|
1677
2240
|
' --changed <repo:path|path> Repeatable explicit changed path; defaults to git status when omitted',
|
|
1678
2241
|
' --json Print the typed docs-update plan as JSON',
|
|
1679
2242
|
'',
|
|
1680
2243
|
'Start-session options:',
|
|
1681
2244
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
1682
|
-
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
|
|
2245
|
+
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd; follows the memory root placement when a marker supplies the root or cwd has no CLAUDE.md under an explicit --root',
|
|
1683
2246
|
' --working-on <text> Required active-session summary',
|
|
1684
2247
|
' --id <lane-id> Required active session lane ID',
|
|
1685
2248
|
' --feature <slug> Repeatable feature slug for the lane',
|
|
1686
2249
|
' --repo <id> Repeatable repo ID for the lane',
|
|
1687
2250
|
' --claim <path> Repeatable path claim for overlap warnings',
|
|
2251
|
+
' --branch <name> Opt-in git binding (D-281): create or reuse this branch in every bound --repo; requires at least one --repo',
|
|
2252
|
+
' --worktree With --branch: provision per-repo worktrees under <workspace>/worktrees/<lane-id>/<repo-id> (D-279) and write the lane marker into the container',
|
|
1688
2253
|
' --date <YYYY-MM-DD> Optional explicit session date',
|
|
1689
2254
|
' --last-thing-completed <text> Optional override for the CLAUDE.md current-session block',
|
|
1690
2255
|
' --blockers <text> Optional current blockers summary',
|
|
1691
2256
|
' --next-session-should <text> Optional current-session handoff summary',
|
|
1692
2257
|
'',
|
|
1693
2258
|
'Close-session options (also accepted by end-session):',
|
|
1694
|
-
' --root <path> Project-memory root.
|
|
1695
|
-
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
|
|
2259
|
+
' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
|
|
2260
|
+
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd; follows the memory root placement when a marker supplies the root or cwd has no CLAUDE.md under an explicit --root',
|
|
1696
2261
|
' --title <text> Required display title for the finalized session note',
|
|
1697
2262
|
' --worked-on <text> Optional override for "What we worked on"',
|
|
1698
|
-
' --session <lane-id> Active session lane to close',
|
|
2263
|
+
' --session <lane-id> Active session lane to close. Omitted: nearest worktree lane marker, else the single active lane; 2+ active lanes require --session or a marker (D-277)',
|
|
1699
2264
|
' --completed <text> Repeatable completed item',
|
|
1700
2265
|
' --decision <text> Repeatable decision reference or summary',
|
|
1701
2266
|
' --model <text> Optional repeatable model contribution entry',
|
|
@@ -1711,6 +2276,35 @@ function usageText() {
|
|
|
1711
2276
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
1712
2277
|
' --session <lane-id> Lane ID for switch-session; positional ID is also accepted',
|
|
1713
2278
|
'',
|
|
2279
|
+
'Rebuild-active-index options:',
|
|
2280
|
+
' --root <path> Project-memory root. Defaults to .compass',
|
|
2281
|
+
' --current <lane-id> Explicit current-lane selection; required when multiple lanes are active and the existing pointer is invalid',
|
|
2282
|
+
'',
|
|
2283
|
+
'Write-lane-marker options (D-280):',
|
|
2284
|
+
' --session <lane-id> Required active lane the marker binds to',
|
|
2285
|
+
' --dir <path> Marker target directory; defaults to cwd. Must be path-disjoint from the memory root',
|
|
2286
|
+
' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
|
|
2287
|
+
'',
|
|
2288
|
+
'Lane-env options (D-282):',
|
|
2289
|
+
' --root <path> Project-memory root. Explicit --root wins; otherwise the nearest worktree lane marker supplies it, else .compass',
|
|
2290
|
+
' --session <lane-id> Lane to export. Omitted: nearest worktree lane marker, else the single active lane; 2+ active lanes require --session or a marker (D-277)',
|
|
2291
|
+
' --json Print the runtime assignment as JSON instead of shell export lines',
|
|
2292
|
+
' --no-conventional Omit the conventional PORT/TMPDIR alias exports',
|
|
2293
|
+
' Consume with: eval "$(vibecompass lane-env)"; defaults come from project.yaml runtime.{port_base,port_step,tmp_base}',
|
|
2294
|
+
'',
|
|
2295
|
+
'Decision-log options (append-decision / next-decision-id):',
|
|
2296
|
+
' --root <path> Project-memory root. Defaults to .compass',
|
|
2297
|
+
' --target <domain.md> Decision domain file under decisions/ (e.g. cross-cutting.md); never INDEX.md',
|
|
2298
|
+
' --entry <path> Staged entry file starting with "### D-NEXT — <title>"; the D-number is allocated atomically at write time (D-276)',
|
|
2299
|
+
' --group <label> Grouped index group label for the D-283 refresh (e.g. "2026-07-03 — Session 3 (lane-x lane)"); defaults to the resolvable lane context',
|
|
2300
|
+
'',
|
|
2301
|
+
'Refresh-decision-index options (D-283):',
|
|
2302
|
+
' --root <path> Project-memory root. Defaults to .compass',
|
|
2303
|
+
' --check Validate the grouped index against canonical decision entries without writing',
|
|
2304
|
+
' --session <lane-id> Lane whose date/session/id labels a new group; defaults to the D-277 lane resolution',
|
|
2305
|
+
' --group <label> Explicit group label for appended rows; wins over the lane-derived label',
|
|
2306
|
+
' Structure-preserving: existing groups/rows are kept verbatim; unparseable structure fails closed',
|
|
2307
|
+
'',
|
|
1714
2308
|
'Sync-agents options:',
|
|
1715
2309
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
1716
2310
|
' --tooling-root <path> Directory where agent files are written. Defaults to cwd',
|