@vibecompass/vibecompass 0.10.5 → 0.10.6
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 +10 -0
- package/README.md +7 -2
- package/package.json +1 -1
- package/src/cli.js +101 -2
- package/src/docs-update.js +538 -0
- package/src/index.js +1 -0
- package/src/project-memory.js +27 -0
- package/src/scaffold.js +29 -6
- package/src/session.js +134 -0
- package/src/status.js +91 -2
- package/src/workflow.js +1 -0
- package/src/workflows/registry.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.10.6 - 2026-06-22
|
|
6
|
+
|
|
7
|
+
- Add `docs-update` as a session-delta documentation maintenance planner that maps changed files, lane claims, session repos/features, new decisions, and package-owned generated/state surfaces to targeted architecture/decision/session follow-up.
|
|
8
|
+
- Print the same docs-update plan during `close-session` / `end-session` before the required document-maintenance checkpoint, while keeping semantic document authorship with the closing agent or human.
|
|
9
|
+
- Breaking: `close-session` / `end-session` and the `closeProjectSession` API now require document-maintenance checkpoint statuses for architecture docs, the decision log, and session handoff/scratch maintenance before a lane can be finalized.
|
|
10
|
+
- Surface shared docs-review architecture quality warnings for missing review metadata, evidence, blindspots, retrieval guidance, and repo scope in scanner/status output.
|
|
11
|
+
- Refresh `state/manifest.json.active_sessions` after session start, switch, and close commands so package-managed roots do not keep stale active-lane summaries.
|
|
12
|
+
|
|
3
13
|
## 0.10.5 - 2026-06-20
|
|
4
14
|
|
|
5
15
|
- Include compact docs-review evidence summaries in hosted review submissions, covering coverage score basis, source-inventory totals, documentation-plan totals, reconciliation deltas, producer stamps, and warning provenance.
|
package/README.md
CHANGED
|
@@ -113,6 +113,9 @@ npx -y @vibecompass/vibecompass@latest close-session \
|
|
|
113
113
|
--session feature-lane \
|
|
114
114
|
--title "Feature Lane" \
|
|
115
115
|
--completed "Shipped the slice" \
|
|
116
|
+
--architecture-docs updated \
|
|
117
|
+
--decision-log not-needed \
|
|
118
|
+
--session-maintenance updated \
|
|
116
119
|
--next-step "Review and publish"
|
|
117
120
|
```
|
|
118
121
|
|
|
@@ -127,12 +130,14 @@ npx -y @vibecompass/vibecompass@latest status --root .compass
|
|
|
127
130
|
npx -y @vibecompass/vibecompass@latest start-session --root .compass --id auth-flow --working-on "Auth flow"
|
|
128
131
|
npx -y @vibecompass/vibecompass@latest list-sessions --root .compass
|
|
129
132
|
npx -y @vibecompass/vibecompass@latest switch-session auth-flow --root .compass
|
|
130
|
-
npx -y @vibecompass/vibecompass@latest
|
|
133
|
+
npx -y @vibecompass/vibecompass@latest docs-update --root .compass --session auth-flow
|
|
134
|
+
npx -y @vibecompass/vibecompass@latest close-session --root .compass --session auth-flow --title "Auth Flow" --completed "Built auth" --architecture-docs updated --decision-log not-needed --session-maintenance updated --next-step "Review"
|
|
131
135
|
|
|
132
136
|
# Agent instruction files
|
|
133
137
|
npx -y @vibecompass/vibecompass@latest sync-agents --root .compass
|
|
134
138
|
|
|
135
|
-
#
|
|
139
|
+
# Targeted and comprehensive architecture docs maintenance
|
|
140
|
+
npx -y @vibecompass/vibecompass@latest docs-update --root .compass --session auth-flow --changed app:src/auth/login.ts
|
|
136
141
|
npx -y @vibecompass/vibecompass@latest docs-review --root .compass --guided
|
|
137
142
|
npx -y @vibecompass/vibecompass@latest docs-review --root .compass --guided --source-root app=../app
|
|
138
143
|
npx -y @vibecompass/vibecompass@latest docs-review --root .compass --apply-output
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { realpathSync } from 'node:fs';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { connectHostedProjectMemory, initializeProjectMemory, setDefaultSyncTarget } from './init.js';
|
|
6
6
|
import { preflightDocsReview } from './docs-review.js';
|
|
7
|
+
import { planDocsUpdate, renderDocsUpdatePlan } from './docs-update.js';
|
|
7
8
|
import { resolveConnectHostedCliOptions, resolveInitCliOptions } from './setup.js';
|
|
8
9
|
import { closeProjectSession, listProjectSessions, startProjectSession, switchProjectSession } from './session.js';
|
|
9
10
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
@@ -205,6 +206,20 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
205
206
|
return 0;
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
if (parsed.command === 'docs-update') {
|
|
210
|
+
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
211
|
+
const result = await planDocsUpdate({
|
|
212
|
+
...parsed.options,
|
|
213
|
+
...(runtime.cwd ? { cwd: runtime.cwd } : {}),
|
|
214
|
+
});
|
|
215
|
+
if (parsed.options.json) {
|
|
216
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
217
|
+
} else {
|
|
218
|
+
io.stdout.write(renderDocsUpdatePlan(result));
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
208
223
|
if (parsed.command === 'close-session' || parsed.command === 'end-session') {
|
|
209
224
|
await writeCompatibilityPreflightWarnings(io, parsed.options, runtime);
|
|
210
225
|
const result = await closeProjectSession({
|
|
@@ -214,12 +229,17 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
214
229
|
io.stdout.write(`Closed session ${result.sessionDate}-${result.sessionNumber}\n`);
|
|
215
230
|
io.stdout.write(`Wrote ${result.sessionFilePath}\n`);
|
|
216
231
|
io.stdout.write(`Updated ${result.claudePath}\n`);
|
|
232
|
+
if (result.docsUpdatePlan) {
|
|
233
|
+
io.stdout.write(renderDocsUpdatePlan(result.docsUpdatePlan));
|
|
234
|
+
}
|
|
235
|
+
writeDocumentMaintenanceCheckpoint(io, result.documentMaintenance);
|
|
217
236
|
if (result.workflowGuidance.length > 0) {
|
|
218
237
|
io.stdout.write('Workflow guidance:\n');
|
|
219
238
|
for (const item of result.workflowGuidance) {
|
|
220
239
|
io.stdout.write(`- ${item}\n`);
|
|
221
240
|
}
|
|
222
241
|
}
|
|
242
|
+
writeWarnings(io, result.warnings);
|
|
223
243
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
224
244
|
return 0;
|
|
225
245
|
}
|
|
@@ -250,6 +270,7 @@ export async function runCli(argv, io = createDefaultIo(), runtime = {}) {
|
|
|
250
270
|
});
|
|
251
271
|
io.stdout.write(`Current session lane: ${result.current}\n`);
|
|
252
272
|
io.stdout.write(`Updated ${result.claudePath}\n`);
|
|
273
|
+
writeWarnings(io, result.warnings);
|
|
253
274
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
254
275
|
return 0;
|
|
255
276
|
}
|
|
@@ -457,6 +478,10 @@ export function parseCliArgs(argv) {
|
|
|
457
478
|
return parseRefreshWorkflowArgs(rest);
|
|
458
479
|
}
|
|
459
480
|
|
|
481
|
+
if (command === 'docs-update') {
|
|
482
|
+
return parseDocsUpdateArgs(rest);
|
|
483
|
+
}
|
|
484
|
+
|
|
460
485
|
if (command === 'close-session' || command === 'end-session') {
|
|
461
486
|
return parseCloseSessionArgs(rest);
|
|
462
487
|
}
|
|
@@ -896,6 +921,7 @@ function parseCloseSessionArgs(argv) {
|
|
|
896
921
|
models: [],
|
|
897
922
|
blockers: [],
|
|
898
923
|
nextSteps: [],
|
|
924
|
+
documentMaintenance: {},
|
|
899
925
|
};
|
|
900
926
|
// Repeatable CLI flags stay singular for readability, but map to plural arrays in the JS API.
|
|
901
927
|
|
|
@@ -949,6 +975,15 @@ function parseCloseSessionArgs(argv) {
|
|
|
949
975
|
case '--next-session-should':
|
|
950
976
|
parsed.nextSessionShould = value;
|
|
951
977
|
break;
|
|
978
|
+
case '--architecture-docs':
|
|
979
|
+
parsed.documentMaintenance.architectureDocs = value;
|
|
980
|
+
break;
|
|
981
|
+
case '--decision-log':
|
|
982
|
+
parsed.documentMaintenance.decisionLog = value;
|
|
983
|
+
break;
|
|
984
|
+
case '--session-maintenance':
|
|
985
|
+
parsed.documentMaintenance.sessionMaintenance = value;
|
|
986
|
+
break;
|
|
952
987
|
default:
|
|
953
988
|
throw new Error(`Unknown flag "${token}".`);
|
|
954
989
|
}
|
|
@@ -960,6 +995,49 @@ function parseCloseSessionArgs(argv) {
|
|
|
960
995
|
};
|
|
961
996
|
}
|
|
962
997
|
|
|
998
|
+
function parseDocsUpdateArgs(argv) {
|
|
999
|
+
const parsed = {
|
|
1000
|
+
changedFiles: [],
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1004
|
+
const token = argv[index];
|
|
1005
|
+
if (token === '--json') {
|
|
1006
|
+
parsed.json = true;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (!token.startsWith('--')) {
|
|
1011
|
+
throw new Error(`Unexpected argument "${token}".`);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const value = argv[index + 1];
|
|
1015
|
+
if (value === undefined) {
|
|
1016
|
+
throw new Error(`Flag "${token}" requires a value.`);
|
|
1017
|
+
}
|
|
1018
|
+
index += 1;
|
|
1019
|
+
|
|
1020
|
+
switch (token) {
|
|
1021
|
+
case '--root':
|
|
1022
|
+
parsed.rootDir = value;
|
|
1023
|
+
break;
|
|
1024
|
+
case '--session':
|
|
1025
|
+
parsed.sessionId = value;
|
|
1026
|
+
break;
|
|
1027
|
+
case '--changed':
|
|
1028
|
+
parsed.changedFiles.push(value);
|
|
1029
|
+
break;
|
|
1030
|
+
default:
|
|
1031
|
+
throw new Error(`Unknown flag "${token}".`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
command: 'docs-update',
|
|
1037
|
+
options: parsed,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
963
1041
|
function parseListSessionsArgs(argv) {
|
|
964
1042
|
const parsed = {};
|
|
965
1043
|
|
|
@@ -1464,6 +1542,17 @@ function writeRefreshWorkflowResult(io, result) {
|
|
|
1464
1542
|
writeAgentFileSyncResult(io, result.agentFileSync);
|
|
1465
1543
|
}
|
|
1466
1544
|
|
|
1545
|
+
function writeDocumentMaintenanceCheckpoint(io, documentMaintenance) {
|
|
1546
|
+
if (!documentMaintenance) {
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
io.stdout.write('Document maintenance checkpoint:\n');
|
|
1551
|
+
io.stdout.write(`- Architecture docs: ${documentMaintenance.architectureDocs}\n`);
|
|
1552
|
+
io.stdout.write(`- Decision log: ${documentMaintenance.decisionLog}\n`);
|
|
1553
|
+
io.stdout.write(`- Session handoff/scratch: ${documentMaintenance.sessionMaintenance}\n`);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1467
1556
|
async function writeCompatibilityPreflightWarnings(io, options = {}, runtime = {}) {
|
|
1468
1557
|
const result = await inspectProjectCompatibility({
|
|
1469
1558
|
...options,
|
|
@@ -1513,9 +1602,10 @@ function usageText() {
|
|
|
1513
1602
|
' vibecompass sync-target [<name>] [options]',
|
|
1514
1603
|
' vibecompass status [options]',
|
|
1515
1604
|
' vibecompass refresh-workflow [--dry-run|--apply] [options]',
|
|
1605
|
+
' vibecompass docs-update [--session <lane-id>] [options]',
|
|
1516
1606
|
' vibecompass start-session --id <lane-id> --working-on <text> [options]',
|
|
1517
|
-
' vibecompass close-session --title <text> --completed <text> --
|
|
1518
|
-
' vibecompass end-session --title <text> --completed <text> --
|
|
1607
|
+
' vibecompass close-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options]',
|
|
1608
|
+
' vibecompass end-session --title <text> --completed <text> --architecture-docs <status> --decision-log <status> --session-maintenance <status> [options] # alias',
|
|
1519
1609
|
' vibecompass list-sessions [options]',
|
|
1520
1610
|
' vibecompass switch-session <id> [options]',
|
|
1521
1611
|
' vibecompass sync-agents [options]',
|
|
@@ -1578,6 +1668,12 @@ function usageText() {
|
|
|
1578
1668
|
' --update-package-stamp Update an existing project.yaml package_version stamp',
|
|
1579
1669
|
' --allow-downgrade-templates Allow applying templates with a CLI older than the root stamp',
|
|
1580
1670
|
'',
|
|
1671
|
+
'Docs-update options:',
|
|
1672
|
+
' --root <path> Project-memory root. Defaults to .compass',
|
|
1673
|
+
' --session <lane-id> Active session lane to inspect; defaults to current lane',
|
|
1674
|
+
' --changed <repo:path|path> Repeatable explicit changed path; defaults to git status when omitted',
|
|
1675
|
+
' --json Print the typed docs-update plan as JSON',
|
|
1676
|
+
'',
|
|
1581
1677
|
'Start-session options:',
|
|
1582
1678
|
' --root <path> Project-memory root. Defaults to .compass',
|
|
1583
1679
|
' --tooling-root <path> Tooling root that contains CLAUDE.md. Defaults to cwd',
|
|
@@ -1602,6 +1698,9 @@ function usageText() {
|
|
|
1602
1698
|
' --model <text> Optional repeatable model contribution entry',
|
|
1603
1699
|
' --blocker <text> Repeatable blocker or open question',
|
|
1604
1700
|
' --next-step <text> Repeatable next-session step',
|
|
1701
|
+
' --architecture-docs <status> Required checkpoint: updated|not-needed|deferred',
|
|
1702
|
+
' --decision-log <status> Required checkpoint: updated|not-needed|deferred',
|
|
1703
|
+
' --session-maintenance <status> Required checkpoint: updated|not-needed|deferred',
|
|
1605
1704
|
' --last-thing-completed <text> Optional override for the CLAUDE.md completed summary',
|
|
1606
1705
|
' --next-session-should <text> Optional override for the CLAUDE.md next-session summary',
|
|
1607
1706
|
'',
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { parseFrontmatter } from './frontmatter.js';
|
|
6
|
+
import { scanProjectMemory } from './project-memory.js';
|
|
7
|
+
import { parseSimpleYaml } from './simple-yaml.js';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const PACKAGE_OWNED_PATH_PATTERNS = [
|
|
11
|
+
/^state\//,
|
|
12
|
+
/^context\.md$/,
|
|
13
|
+
/^workflows\//,
|
|
14
|
+
/^CLAUDE\.md$/,
|
|
15
|
+
/^AGENTS\.md$/,
|
|
16
|
+
/^\.cursorrules$/,
|
|
17
|
+
/^\.github\/copilot-instructions\.md$/,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export async function planDocsUpdate(options = {}) {
|
|
21
|
+
const normalized = normalizeDocsUpdateOptions(options);
|
|
22
|
+
const scan = await scanProjectMemory(normalized.rootDir);
|
|
23
|
+
const project = scan.project?.extracted ?? {};
|
|
24
|
+
const activeSession = await readActiveSession(normalized.rootDir, normalized.sessionId);
|
|
25
|
+
const changedFiles = await resolveChangedFiles({
|
|
26
|
+
cwd: normalized.cwd,
|
|
27
|
+
explicitChangedFiles: normalized.changedFiles,
|
|
28
|
+
repos: project.repos ?? [],
|
|
29
|
+
});
|
|
30
|
+
const architectureDocs = scan.documents
|
|
31
|
+
.filter((document) => document.kind === 'architecture')
|
|
32
|
+
.map((document) => summarizeArchitectureDocument(document));
|
|
33
|
+
const delta = {
|
|
34
|
+
changedFiles,
|
|
35
|
+
claimedPaths: activeSession?.claimedPaths ?? [],
|
|
36
|
+
sessionRepos: activeSession?.repos ?? [],
|
|
37
|
+
featureSlugs: activeSession?.featureSlugs ?? [],
|
|
38
|
+
};
|
|
39
|
+
const affectedArchitectureDocs = findAffectedArchitectureDocs(architectureDocs, delta);
|
|
40
|
+
const packageOwnedChanges = changedFiles
|
|
41
|
+
.filter((file) => isPackageOwnedPath(file.normalizedPath))
|
|
42
|
+
.map((file) => file.raw);
|
|
43
|
+
const decisionStatus = summarizeDecisionStatus(scan, activeSession);
|
|
44
|
+
const recommendations = buildRecommendations({
|
|
45
|
+
changedFiles,
|
|
46
|
+
affectedArchitectureDocs,
|
|
47
|
+
packageOwnedChanges,
|
|
48
|
+
decisionStatus,
|
|
49
|
+
activeSession,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
rootDir: normalized.rootDir,
|
|
54
|
+
cwd: normalized.cwd,
|
|
55
|
+
session: activeSession
|
|
56
|
+
? {
|
|
57
|
+
id: activeSession.id,
|
|
58
|
+
workingOn: activeSession.workingOn,
|
|
59
|
+
decisionSnapshotHighestId: activeSession.decisionSnapshotHighestId,
|
|
60
|
+
}
|
|
61
|
+
: null,
|
|
62
|
+
delta: {
|
|
63
|
+
changedFiles: changedFiles.map((file) => file.raw),
|
|
64
|
+
claimedPaths: delta.claimedPaths,
|
|
65
|
+
sessionRepos: delta.sessionRepos,
|
|
66
|
+
featureSlugs: delta.featureSlugs,
|
|
67
|
+
},
|
|
68
|
+
architecture: {
|
|
69
|
+
affected: affectedArchitectureDocs,
|
|
70
|
+
needsNewDoc: changedFiles.some((file) => isImplementationLikePath(file.normalizedPath)) && affectedArchitectureDocs.length === 0,
|
|
71
|
+
},
|
|
72
|
+
decisions: decisionStatus,
|
|
73
|
+
packageOwnedChanges,
|
|
74
|
+
recommendations,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function renderDocsUpdatePlan(plan) {
|
|
79
|
+
const lines = [
|
|
80
|
+
'Docs update plan:',
|
|
81
|
+
`- Session: ${plan.session?.id ?? '(none selected)'}`,
|
|
82
|
+
`- Changed files: ${plan.delta.changedFiles.length > 0 ? plan.delta.changedFiles.join(', ') : 'none detected'}`,
|
|
83
|
+
`- Claimed paths: ${plan.delta.claimedPaths.length > 0 ? plan.delta.claimedPaths.join(', ') : 'none recorded'}`,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
lines.push('Affected architecture docs:');
|
|
87
|
+
if (plan.architecture.affected.length === 0) {
|
|
88
|
+
lines.push(plan.architecture.needsNewDoc
|
|
89
|
+
? '- No matching architecture doc found for implementation-like changes; create a focused component doc or defer explicitly.'
|
|
90
|
+
: '- None detected from the current session delta.');
|
|
91
|
+
} else {
|
|
92
|
+
for (const doc of plan.architecture.affected) {
|
|
93
|
+
lines.push(`- ${doc.path}`);
|
|
94
|
+
for (const reason of doc.reasons) {
|
|
95
|
+
lines.push(` - ${reason}`);
|
|
96
|
+
}
|
|
97
|
+
if (doc.qualityWarnings.length > 0) {
|
|
98
|
+
lines.push(` - quality warnings: ${doc.qualityWarnings.map((warning) => warning.code).join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
lines.push('Decision log:');
|
|
104
|
+
if (plan.decisions.newDecisionIds.length > 0) {
|
|
105
|
+
lines.push(`- New decisions since lane start: ${plan.decisions.newDecisionIds.map((id) => `D-${id}`).join(', ')}`);
|
|
106
|
+
} else if (plan.decisions.decisionSnapshotHighestId === null) {
|
|
107
|
+
lines.push('- No lane decision snapshot available.');
|
|
108
|
+
} else {
|
|
109
|
+
lines.push('- No new decisions detected since lane start.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (plan.packageOwnedChanges.length > 0) {
|
|
113
|
+
lines.push('Package-owned generated/state surfaces:');
|
|
114
|
+
for (const filePath of plan.packageOwnedChanges) {
|
|
115
|
+
lines.push(`- ${filePath}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lines.push('Recommended next actions:');
|
|
120
|
+
for (const recommendation of plan.recommendations) {
|
|
121
|
+
lines.push(`- ${recommendation}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return `${lines.join('\n')}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeDocsUpdateOptions(options) {
|
|
128
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
129
|
+
return {
|
|
130
|
+
cwd,
|
|
131
|
+
rootDir: path.resolve(cwd, options.rootDir ?? '.compass'),
|
|
132
|
+
sessionId: normalizeOptionalString(options.sessionId),
|
|
133
|
+
changedFiles: Array.isArray(options.changedFiles) ? options.changedFiles : [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function readActiveSession(rootDir, requestedSessionId) {
|
|
138
|
+
const activeRoot = path.join(rootDir, 'sessions', 'active');
|
|
139
|
+
const indexPath = path.join(activeRoot, 'index.yaml');
|
|
140
|
+
let sessionId = normalizeOptionalString(requestedSessionId);
|
|
141
|
+
|
|
142
|
+
if (!sessionId) {
|
|
143
|
+
try {
|
|
144
|
+
const indexData = parseSimpleYaml(await readFile(indexPath, 'utf8'), { sourceName: indexPath });
|
|
145
|
+
sessionId = normalizeOptionalString(indexData.current);
|
|
146
|
+
} catch {
|
|
147
|
+
sessionId = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!sessionId) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sessionPath = path.join(activeRoot, sessionId, 'session.yaml');
|
|
156
|
+
try {
|
|
157
|
+
const data = parseSimpleYaml(await readFile(sessionPath, 'utf8'), { sourceName: sessionPath });
|
|
158
|
+
return {
|
|
159
|
+
id: sessionId,
|
|
160
|
+
workingOn: normalizeOptionalString(data.working_on),
|
|
161
|
+
repos: normalizeStringArray(data.repos),
|
|
162
|
+
claimedPaths: normalizeStringArray(data.claimed_paths),
|
|
163
|
+
featureSlugs: normalizeStringArray(data.feature_slugs),
|
|
164
|
+
decisionSnapshotHighestId: Number.isInteger(data.decision_snapshot?.highest_decision_id)
|
|
165
|
+
? data.decision_snapshot.highest_decision_id
|
|
166
|
+
: null,
|
|
167
|
+
};
|
|
168
|
+
} catch {
|
|
169
|
+
return {
|
|
170
|
+
id: sessionId,
|
|
171
|
+
workingOn: null,
|
|
172
|
+
repos: [],
|
|
173
|
+
claimedPaths: [],
|
|
174
|
+
featureSlugs: [],
|
|
175
|
+
decisionSnapshotHighestId: null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function resolveChangedFiles(options) {
|
|
181
|
+
if (options.explicitChangedFiles.length > 0) {
|
|
182
|
+
return normalizeChangedFiles(options.explicitChangedFiles, options.repos);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const changedPaths = new Set();
|
|
186
|
+
for (const filePath of await readGitStatusPaths(options.cwd)) {
|
|
187
|
+
changedPaths.add(filePath);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const repoStatusResults = await Promise.all(
|
|
191
|
+
options.repos.map(async (repo) => {
|
|
192
|
+
const repoDir = resolveRepoWorkingDirectory(options.cwd, repo);
|
|
193
|
+
if (!repo?.id || !repoDir || repoDir === options.cwd) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (await readGitStatusPaths(repoDir)).map((filePath) => `${repo.id}:${filePath}`);
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
for (const filePath of repoStatusResults.flat()) {
|
|
202
|
+
changedPaths.add(filePath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return normalizeChangedFiles(Array.from(changedPaths).sort((left, right) => left.localeCompare(right)), options.repos);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function readGitStatusPaths(cwd) {
|
|
209
|
+
try {
|
|
210
|
+
const { stdout } = await execFileAsync('git', ['-C', cwd, 'status', '--porcelain=v1', '--untracked-files=all'], {
|
|
211
|
+
timeout: 5000,
|
|
212
|
+
maxBuffer: 1024 * 1024,
|
|
213
|
+
});
|
|
214
|
+
return parseGitStatusPaths(stdout);
|
|
215
|
+
} catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function resolveRepoWorkingDirectory(cwd, repo) {
|
|
221
|
+
if (!repo?.id) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (repo.path) {
|
|
226
|
+
return path.resolve(cwd, repo.path);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return path.resolve(cwd, repo.id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseGitStatusPaths(stdout) {
|
|
233
|
+
return stdout
|
|
234
|
+
.split(/\r?\n/)
|
|
235
|
+
.map((line) => line.trimEnd())
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
.map((line) => {
|
|
238
|
+
const rawPath = line.slice(3).trim();
|
|
239
|
+
const renameIndex = findUnquotedRenameSeparator(rawPath);
|
|
240
|
+
const pathPart = renameIndex >= 0 ? rawPath.slice(renameIndex + 4).trim() : rawPath;
|
|
241
|
+
return unquoteGitPorcelainPath(pathPart);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findUnquotedRenameSeparator(value) {
|
|
246
|
+
let inQuotes = false;
|
|
247
|
+
let escaped = false;
|
|
248
|
+
|
|
249
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
250
|
+
const char = value[index];
|
|
251
|
+
if (escaped) {
|
|
252
|
+
escaped = false;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (inQuotes && char === '\\') {
|
|
257
|
+
escaped = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (char === '"') {
|
|
262
|
+
inQuotes = !inQuotes;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!inQuotes && value.startsWith(' -> ', index)) {
|
|
267
|
+
return index;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return -1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function unquoteGitPorcelainPath(value) {
|
|
275
|
+
const trimmed = value.trim();
|
|
276
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
|
|
277
|
+
return trimmed;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let decoded = '';
|
|
281
|
+
let octalBytes = [];
|
|
282
|
+
const flushOctalBytes = () => {
|
|
283
|
+
if (octalBytes.length === 0) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
decoded += Buffer.from(octalBytes).toString('utf8');
|
|
288
|
+
octalBytes = [];
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
for (let index = 1; index < trimmed.length - 1; index += 1) {
|
|
292
|
+
const char = trimmed[index];
|
|
293
|
+
if (char !== '\\') {
|
|
294
|
+
flushOctalBytes();
|
|
295
|
+
decoded += char;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const next = trimmed[index + 1];
|
|
300
|
+
if (next === undefined) {
|
|
301
|
+
flushOctalBytes();
|
|
302
|
+
decoded += '\\';
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const octalMatch = trimmed.slice(index + 1).match(/^[0-7]{1,3}/);
|
|
307
|
+
if (octalMatch) {
|
|
308
|
+
octalBytes.push(Number.parseInt(octalMatch[0], 8));
|
|
309
|
+
index += octalMatch[0].length;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
flushOctalBytes();
|
|
314
|
+
const escapes = {
|
|
315
|
+
a: '\u0007',
|
|
316
|
+
b: '\b',
|
|
317
|
+
f: '\f',
|
|
318
|
+
n: '\n',
|
|
319
|
+
r: '\r',
|
|
320
|
+
t: '\t',
|
|
321
|
+
v: '\v',
|
|
322
|
+
'\\': '\\',
|
|
323
|
+
'"': '"',
|
|
324
|
+
};
|
|
325
|
+
decoded += escapes[next] ?? next;
|
|
326
|
+
index += 1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
flushOctalBytes();
|
|
330
|
+
return decoded;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function normalizeChangedFiles(paths, repos) {
|
|
334
|
+
return paths
|
|
335
|
+
.map((raw) => normalizeOptionalString(raw))
|
|
336
|
+
.filter(Boolean)
|
|
337
|
+
.map((raw) => {
|
|
338
|
+
const normalizedPath = normalizePath(raw);
|
|
339
|
+
return {
|
|
340
|
+
raw,
|
|
341
|
+
normalizedPath,
|
|
342
|
+
repoPathCandidates: buildRepoPathCandidates(normalizedPath, repos),
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildRepoPathCandidates(filePath, repos) {
|
|
348
|
+
const candidates = new Set([filePath]);
|
|
349
|
+
const explicitRepoMatch = filePath.match(/^([^:/]+):(.+)$/);
|
|
350
|
+
if (explicitRepoMatch) {
|
|
351
|
+
const repoRelativePath = normalizePath(explicitRepoMatch[2]);
|
|
352
|
+
candidates.add(`${explicitRepoMatch[1]}:${repoRelativePath}`);
|
|
353
|
+
candidates.add(repoRelativePath);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!explicitRepoMatch && repos.length === 1 && repos[0]?.id) {
|
|
357
|
+
candidates.add(`${repos[0].id}:${filePath}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const repo of repos) {
|
|
361
|
+
if (!repo?.id) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (filePath.startsWith(`${repo.id}/`)) {
|
|
365
|
+
candidates.add(`${repo.id}:${filePath.slice(repo.id.length + 1)}`);
|
|
366
|
+
}
|
|
367
|
+
if (repo.path) {
|
|
368
|
+
const repoPath = normalizePath(repo.path).replace(/^\.\//, '');
|
|
369
|
+
if (repoPath && filePath.startsWith(`${repoPath}/`)) {
|
|
370
|
+
candidates.add(`${repo.id}:${filePath.slice(repoPath.length + 1)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return Array.from(candidates);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function summarizeArchitectureDocument(document) {
|
|
379
|
+
const frontmatter = parseFrontmatter(document.content, { sourceName: document.path });
|
|
380
|
+
const data = frontmatter.data ?? {};
|
|
381
|
+
return {
|
|
382
|
+
path: document.path,
|
|
383
|
+
repoIds: [
|
|
384
|
+
...(typeof data.repo === 'string' ? [data.repo] : []),
|
|
385
|
+
...(Array.isArray(data.repos) ? data.repos.filter((repo) => typeof repo === 'string') : []),
|
|
386
|
+
],
|
|
387
|
+
domain: normalizeOptionalString(data.domain),
|
|
388
|
+
feature: normalizeOptionalString(data.feature),
|
|
389
|
+
component: normalizeOptionalString(data.component),
|
|
390
|
+
involvedFiles: extractInvolvedFiles(frontmatter.body),
|
|
391
|
+
warnings: document.warnings,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function extractInvolvedFiles(body) {
|
|
396
|
+
return Array.from(body.matchAll(/`([^`\n]+:[^`\n]+)`/g))
|
|
397
|
+
.map((match) => normalizePath(match[1]))
|
|
398
|
+
.filter(Boolean);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function findAffectedArchitectureDocs(docs, delta) {
|
|
402
|
+
return docs
|
|
403
|
+
.map((doc) => {
|
|
404
|
+
const reasons = [];
|
|
405
|
+
|
|
406
|
+
for (const changedFile of delta.changedFiles) {
|
|
407
|
+
if (pathMatchesDocument(changedFile, doc)) {
|
|
408
|
+
reasons.push(`matches changed file ${changedFile.raw}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const claimedPath of delta.claimedPaths) {
|
|
413
|
+
const normalizedClaim = normalizePath(claimedPath);
|
|
414
|
+
if (doc.involvedFiles.some((involvedFile) => pathsOverlap(involvedFile, normalizedClaim))) {
|
|
415
|
+
reasons.push(`matches lane claim ${claimedPath}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (delta.featureSlugs.some((feature) => slugify(doc.feature ?? '') === feature)) {
|
|
420
|
+
reasons.push(`matches lane feature ${delta.featureSlugs.filter((feature) => slugify(doc.feature ?? '') === feature).join(', ')}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
path: doc.path,
|
|
425
|
+
reasons: Array.from(new Set(reasons)),
|
|
426
|
+
qualityWarnings: doc.warnings,
|
|
427
|
+
};
|
|
428
|
+
})
|
|
429
|
+
.filter((doc) => doc.reasons.length > 0)
|
|
430
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function pathMatchesDocument(changedFile, doc) {
|
|
434
|
+
if (changedFile.normalizedPath === doc.path) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return changedFile.repoPathCandidates.some((candidate) =>
|
|
439
|
+
doc.involvedFiles.some((involvedFile) => pathsOverlap(involvedFile, candidate)),
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function pathsOverlap(left, right) {
|
|
444
|
+
const normalizedLeft = normalizePath(left);
|
|
445
|
+
const normalizedRight = normalizePath(right);
|
|
446
|
+
return (
|
|
447
|
+
normalizedLeft === normalizedRight ||
|
|
448
|
+
normalizedLeft.startsWith(`${normalizedRight}/`) ||
|
|
449
|
+
normalizedRight.startsWith(`${normalizedLeft}/`) ||
|
|
450
|
+
normalizedLeft.endsWith(`/${normalizedRight}`) ||
|
|
451
|
+
normalizedRight.endsWith(`/${normalizedLeft}`)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function summarizeDecisionStatus(scan, activeSession) {
|
|
456
|
+
const decisionIds = scan.documents
|
|
457
|
+
.filter((document) => document.kind === 'decision')
|
|
458
|
+
.flatMap((document) => document.extracted?.decision_ids ?? [])
|
|
459
|
+
.sort((left, right) => left - right);
|
|
460
|
+
const highestDecisionId = decisionIds.length > 0 ? decisionIds[decisionIds.length - 1] : null;
|
|
461
|
+
const snapshot = activeSession?.decisionSnapshotHighestId ?? null;
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
highestDecisionId,
|
|
465
|
+
decisionSnapshotHighestId: snapshot,
|
|
466
|
+
newDecisionIds: snapshot === null ? [] : decisionIds.filter((id) => id > snapshot),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function buildRecommendations(options) {
|
|
471
|
+
const recommendations = [];
|
|
472
|
+
|
|
473
|
+
if (options.affectedArchitectureDocs.length > 0) {
|
|
474
|
+
recommendations.push('Review and update the affected architecture docs listed above, then close with --architecture-docs updated.');
|
|
475
|
+
} else if (options.changedFiles.length > 0 && options.affectedArchitectureDocs.length === 0) {
|
|
476
|
+
recommendations.push('No matching architecture doc was found for the session delta; create a focused doc, mark docs not-needed with evidence, or defer explicitly.');
|
|
477
|
+
} else {
|
|
478
|
+
recommendations.push('No architecture doc update is indicated by the current delta; close with --architecture-docs not-needed if that remains true.');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (options.decisionStatus.newDecisionIds.length > 0) {
|
|
482
|
+
recommendations.push('Ensure affected architecture docs reference the new decisions where they change contracts or ownership.');
|
|
483
|
+
} else {
|
|
484
|
+
recommendations.push('Append a decision only if this session accepted a real architectural/product/process choice.');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (options.packageOwnedChanges.length > 0) {
|
|
488
|
+
recommendations.push('Do not hand-edit package-owned state/generated surfaces; use refresh-workflow --dry-run/--apply or sync-agents as appropriate.');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (options.activeSession) {
|
|
492
|
+
recommendations.push('Keep the active lane wip.md/handoff.md current; close-session will distill them into the finalized note.');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return recommendations;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function isPackageOwnedPath(filePath) {
|
|
499
|
+
const normalized = normalizePath(filePath).replace(/^\.compass\//, '');
|
|
500
|
+
return PACKAGE_OWNED_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function isImplementationLikePath(filePath) {
|
|
504
|
+
const normalized = stripRepoPrefix(filePath);
|
|
505
|
+
return !(
|
|
506
|
+
normalized.startsWith('architecture/') ||
|
|
507
|
+
normalized.startsWith('decisions/') ||
|
|
508
|
+
normalized.startsWith('sessions/') ||
|
|
509
|
+
isPackageOwnedPath(normalized)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function stripRepoPrefix(filePath) {
|
|
514
|
+
const normalized = normalizePath(filePath);
|
|
515
|
+
const repoMatch = normalized.match(/^[^:/]+:(.+)$/);
|
|
516
|
+
return repoMatch ? normalizePath(repoMatch[1]) : normalized;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function normalizeStringArray(value) {
|
|
520
|
+
return Array.isArray(value)
|
|
521
|
+
? value.map((item) => normalizeOptionalString(item)).filter(Boolean)
|
|
522
|
+
: [];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function normalizeOptionalString(value) {
|
|
526
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function normalizePath(value) {
|
|
530
|
+
return String(value).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function slugify(value) {
|
|
534
|
+
return value
|
|
535
|
+
.toLowerCase()
|
|
536
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
537
|
+
.replace(/^-+|-+$/g, '');
|
|
538
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { refreshWorkflow } from './refresh-workflow.js';
|
|
|
5
5
|
export { getProjectStatus, renderStatusText, toStatusJson } from './status.js';
|
|
6
6
|
export { initializeProjectMemory } from './init.js';
|
|
7
7
|
export { preflightDocsReview } from './docs-review.js';
|
|
8
|
+
export { planDocsUpdate, renderDocsUpdatePlan } from './docs-update.js';
|
|
8
9
|
export {
|
|
9
10
|
applyPullExport,
|
|
10
11
|
pullExportProjectMemory,
|
package/src/project-memory.js
CHANGED
|
@@ -11,7 +11,9 @@ const NON_CANONICAL_ARCHITECTURE_FILES = new Set(['README.md']);
|
|
|
11
11
|
const VALID_MODES = new Set(['local-only', 'local-primary', 'hosted-only']);
|
|
12
12
|
const RECOMMENDED_ARCHITECTURE_SECTIONS = [
|
|
13
13
|
'Description',
|
|
14
|
+
'Review metadata',
|
|
14
15
|
'Details',
|
|
16
|
+
'Retrieval guidance',
|
|
15
17
|
'Next steps',
|
|
16
18
|
'Involved files',
|
|
17
19
|
];
|
|
@@ -446,6 +448,31 @@ function parseArchitectureDocument(options) {
|
|
|
446
448
|
}
|
|
447
449
|
}
|
|
448
450
|
|
|
451
|
+
if (!/^\s*-\s*Evidence:\s+\S/im.test(frontmatter.body)) {
|
|
452
|
+
warnings.push(
|
|
453
|
+
createWarning(
|
|
454
|
+
'architecture-missing-evidence-metadata',
|
|
455
|
+
'Architecture doc is missing review metadata "Evidence" with concrete repo:path references.',
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
} else if (/^\s*-\s*Evidence:\s*repo:path references inspected before making implementation claims\s*$/im.test(frontmatter.body)) {
|
|
459
|
+
warnings.push(
|
|
460
|
+
createWarning(
|
|
461
|
+
'architecture-generic-evidence-metadata',
|
|
462
|
+
'Architecture doc uses generic evidence metadata; list concrete repo:path references instead.',
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!/^\s*-\s*Blindspots:\s+\S/im.test(frontmatter.body)) {
|
|
468
|
+
warnings.push(
|
|
469
|
+
createWarning(
|
|
470
|
+
'architecture-missing-blindspots-metadata',
|
|
471
|
+
'Architecture doc is missing review metadata "Blindspots".',
|
|
472
|
+
),
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
449
476
|
if (repoIds.length === 0 && /##\s+Involved files/i.test(frontmatter.body) && /`[^`]+\/[^`]+`/.test(frontmatter.body)) {
|
|
450
477
|
warnings.push(
|
|
451
478
|
createWarning(
|
package/src/scaffold.js
CHANGED
|
@@ -216,7 +216,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
216
216
|
## Canonical files
|
|
217
217
|
- \`${rootRelativePath}/project.yaml\` — machine-oriented project metadata
|
|
218
218
|
- \`${rootRelativePath}/architecture/\` — canonical architecture docs
|
|
219
|
-
- \`${rootRelativePath}/decisions/\` —
|
|
219
|
+
- \`${rootRelativePath}/decisions/\` — append-only decision log for this project-memory root
|
|
220
220
|
- \`${rootRelativePath}/sessions/\` — finalized session notes
|
|
221
221
|
|
|
222
222
|
## Derived and scratch files
|
|
@@ -233,7 +233,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
233
233
|
- The active lane scratch files live under \`${rootRelativePath}/sessions/active/<lane-id>/\`.
|
|
234
234
|
- \`${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.
|
|
235
235
|
- Finalized sessions are append-only notes named \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; multiple sessions on the same day increment \`N\`.
|
|
236
|
-
- 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
|
|
236
|
+
- 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 decision record for this root.
|
|
237
237
|
|
|
238
238
|
## Repos in scope
|
|
239
239
|
${repos}
|
|
@@ -248,7 +248,7 @@ ${repos}
|
|
|
248
248
|
${promptCommands}
|
|
249
249
|
|
|
250
250
|
These are prompt commands for agent behavior, not \`vibecompass\` CLI subcommands.
|
|
251
|
-
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id
|
|
251
|
+
Reviewer handback is explicit: the reviewer ends the pass by updating the selected lane's \`wip.md\` + \`handoff.md\` and then stopping. The \`close session\` prompt runs builder close-out and ends with \`vibecompass close-session --session <lane-id>\` plus document-maintenance checkpoint statuses, which follows the workflow defaults recorded in \`${rootRelativePath}/project.yaml\`.
|
|
252
252
|
\`vibecompass end-session\` is also accepted as an alias for \`vibecompass close-session\`; the canonical command name remains \`close-session\`.
|
|
253
253
|
|
|
254
254
|
## Workflow defaults
|
|
@@ -257,6 +257,7 @@ ${renderWorkflowDefaults(workflow)}
|
|
|
257
257
|
## Documentation coverage
|
|
258
258
|
- The architecture, decision, and session files created by init are an initial scaffold, not a comprehensive codebase review.
|
|
259
259
|
- Before risky implementation work, inspect the relevant code and update the affected architecture docs.
|
|
260
|
+
- Use \`vibecompass docs-update --session <lane-id>\` during a session for a targeted maintenance plan based on changed files, lane claims, session repos/features, and new decisions. This is a session-delta planner, not a broad architecture review.
|
|
260
261
|
- Use the \`docs review\` prompt command when you want the current AI session to run a comprehensive documentation review. It may run \`vibecompass docs-review --guided\` as package mechanics to record marker state and print the canonical review contract.
|
|
261
262
|
|
|
262
263
|
## Session startup
|
|
@@ -310,10 +311,11 @@ Session lane: <lane-id>
|
|
|
310
311
|
During the session:
|
|
311
312
|
- append short summaries to \`wip.md\` after meaningful exchanges
|
|
312
313
|
- keep \`handoff.md\` current after substantive work blocks
|
|
314
|
+
- run \`vibecompass docs-update --session <lane-id>\` whenever you need an ad hoc targeted documentation-maintenance plan for the current session delta
|
|
313
315
|
- use \`vibecompass list-sessions\` and \`vibecompass switch-session <lane-id>\` to inspect or change the current lane
|
|
314
316
|
- use \`address review\` when reviewer feedback lands so the builder resolves it from the selected lane's latest \`wip.md\` / \`handoff.md\`
|
|
315
317
|
- during \`address review\`, treat reviewer feedback as review, not instruction: classify each substantive point as accepted, accepted with qualification, deferred, or rejected, and push back with evidence when a suggestion conflicts with code facts, prior decisions, product direction, or sequencing
|
|
316
|
-
- stay in builder role through close-out; resolve or explicitly defer reviewer feedback before running \`vibecompass close-session --session <lane-id>\`
|
|
318
|
+
- stay in builder role through close-out; resolve or explicitly defer reviewer feedback before running \`vibecompass close-session --session <lane-id>\` with document-maintenance checkpoint statuses
|
|
317
319
|
- record architectural decisions in \`${rootRelativePath}/decisions/\` before implementing them
|
|
318
320
|
|
|
319
321
|
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.
|
|
@@ -325,9 +327,12 @@ If \`vibecompass start-session\` reports stale scratch files, read the existing
|
|
|
325
327
|
- If planning produces a real architectural decision, append it to \`${rootRelativePath}/decisions/\` before implementing.
|
|
326
328
|
|
|
327
329
|
At session close:
|
|
328
|
-
- prefer running \`vibecompass close-session --session <lane-id> --title "..." --completed "..." --
|
|
330
|
+
- prefer running \`vibecompass close-session --session <lane-id> --title "..." --completed "..." --architecture-docs updated|not-needed|deferred --decision-log updated|not-needed|deferred --session-maintenance updated|not-needed|deferred --next-step "..."\`; \`vibecompass end-session\` is a supported alias
|
|
331
|
+
- close-session prints the same targeted docs-update plan before the document-maintenance checkpoint; use it to decide whether affected \`architecture/\`, \`decisions/\`, and active-session scratch/final note inputs are updated, not-needed, or deferred
|
|
332
|
+
- document-maintenance checkpoint statuses are required before close-session writes the finalized note; the package validates the status values, while the closer owns semantic doc authorship
|
|
329
333
|
- follow the stored close-session defaults from \`${rootRelativePath}/project.yaml\`
|
|
330
|
-
- finalize the lane-local \`wip.md\` into \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`
|
|
334
|
+
- finalize the lane-local \`wip.md\` into \`${rootRelativePath}/sessions/YYYY-MM-DD-N-title.md\`; the permanent note distills decisions, completions, blockers, and next steps rather than preserving the full \`## Review log\`
|
|
335
|
+
- if a granular reviewer trail must remain durable, summarize it explicitly in the session note inputs or create a separate finalized session note before close-session deletes lane scratch files
|
|
331
336
|
- delete the closed lane directory under \`${rootRelativePath}/sessions/active/<lane-id>/\`
|
|
332
337
|
- refresh any affected architecture/decision docs
|
|
333
338
|
|
|
@@ -371,6 +376,10 @@ Initial project-memory scaffold for ${projectConfig.name}.
|
|
|
371
376
|
|
|
372
377
|
${projectConfig.description ? `${projectConfig.description}\n` : ''}This document is a starting map, not a comprehensive architecture review.
|
|
373
378
|
|
|
379
|
+
## Review metadata
|
|
380
|
+
- Evidence: \`project.yaml\`, declared repo descriptors, and the files created by \`vibecompass init\`
|
|
381
|
+
- Blindspots: Runtime architecture, feature flows, persistence, integrations, deployment, observability, and test strategy are not reviewed by the init scaffold.
|
|
382
|
+
|
|
374
383
|
## Details
|
|
375
384
|
- Mode: \`${projectConfig.mode}\`
|
|
376
385
|
- Documentation coverage: initial scaffold
|
|
@@ -394,6 +403,11 @@ Not yet documented:
|
|
|
394
403
|
|
|
395
404
|
Before changing any undocumented area, inspect the relevant code and update or add architecture docs.
|
|
396
405
|
|
|
406
|
+
## Retrieval guidance
|
|
407
|
+
- Use this doc to understand the project-memory root shape, declared repositories, and the fact that the initial coverage is scaffold-only.
|
|
408
|
+
- Do not use this doc as evidence for runtime behavior, feature ownership, data flow, external integrations, or deployment behavior.
|
|
409
|
+
- Prefer more specific domain/feature/component docs once they exist, and run \`vibecompass docs-update --session <lane-id>\` during active work to find targeted maintenance needs.
|
|
410
|
+
|
|
397
411
|
## Next steps
|
|
398
412
|
- Use the \`docs review\` prompt command for a comprehensive documentation review.
|
|
399
413
|
- Replace this placeholder with domain/feature/component docs as the project map becomes evidence-backed.
|
|
@@ -487,10 +501,18 @@ architecture/<domain-slug>/<feature-slug>/<component>.md
|
|
|
487
501
|
|
|
488
502
|
## Recommended sections
|
|
489
503
|
- \`## Description\`
|
|
504
|
+
- \`## Review metadata\`
|
|
490
505
|
- \`## Details\`
|
|
506
|
+
- \`## Retrieval guidance\`
|
|
491
507
|
- \`## Next steps\`
|
|
492
508
|
- \`## Involved files\`
|
|
493
509
|
|
|
510
|
+
## Shared quality bar
|
|
511
|
+
- Review metadata should record concrete evidence sources and blindspots.
|
|
512
|
+
- Retrieval guidance should say when to consult the doc, what it does not cover, and which related docs or decisions matter.
|
|
513
|
+
- Details should distinguish confirmed behavior from known gaps or open follow-up.
|
|
514
|
+
- Involved files should use concrete \`repo:path\` references that match the affected implementation surface.
|
|
515
|
+
|
|
494
516
|
Each component doc is canonical. This README is only a convenience guide.
|
|
495
517
|
`;
|
|
496
518
|
}
|
|
@@ -600,6 +622,7 @@ function buildScaffoldAgentContext(options) {
|
|
|
600
622
|
function renderWorkflowDefaults(workflow) {
|
|
601
623
|
const lines = [
|
|
602
624
|
`- reviewer handback: ${describeReviewerHandback(workflow)}`,
|
|
625
|
+
'- close-session: require document-maintenance checkpoint statuses for architecture docs, decision log, and session handoff/scratch',
|
|
603
626
|
'- close-session: refresh any relevant architecture docs before finalizing the session',
|
|
604
627
|
'- close-session: refresh any relevant decision files before finalizing the session',
|
|
605
628
|
];
|
package/src/session.js
CHANGED
|
@@ -5,11 +5,34 @@ import { parseSimpleYaml } from './simple-yaml.js';
|
|
|
5
5
|
import { buildCloseSessionGuidance, resolveWorkflowSettings } from './workflow.js';
|
|
6
6
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
7
7
|
import { START_MARKER } from './generators/agent-files/markers.js';
|
|
8
|
+
import { planDocsUpdate } from './docs-update.js';
|
|
9
|
+
// This lazy-only cycle is intentional: manifest.js reads lane state from this
|
|
10
|
+
// module, and session command handlers rewrite the derived manifest after
|
|
11
|
+
// lane mutations. Keep both sides free of top-level calls into the other.
|
|
12
|
+
import { writeStateManifest } from './manifest.js';
|
|
8
13
|
|
|
9
14
|
const CURRENT_SESSION_REQUIRED_FIELDS = ['Date:', 'Working on:', 'Last thing completed:', 'Blockers:', 'Next session should:'];
|
|
10
15
|
const WIP_HEADER_PATTERN = /^# WIP — (\d{4}-\d{2}-\d{2}) \(session (\d+)\)$/m;
|
|
11
16
|
const SESSION_FILENAME_PATTERN = /^(\d{4}-\d{2}-\d{2})-(\d+)-([a-z0-9-]+)\.md$/i;
|
|
12
17
|
const LANE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
18
|
+
const DOCUMENT_MAINTENANCE_STATUSES = new Set(['updated', 'not-needed', 'deferred']);
|
|
19
|
+
const DOCUMENT_MAINTENANCE_FIELDS = [
|
|
20
|
+
{
|
|
21
|
+
key: 'architectureDocs',
|
|
22
|
+
label: 'Architecture docs',
|
|
23
|
+
flag: '--architecture-docs',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'decisionLog',
|
|
27
|
+
label: 'Decision log',
|
|
28
|
+
flag: '--decision-log',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'sessionMaintenance',
|
|
32
|
+
label: 'Session handoff/scratch',
|
|
33
|
+
flag: '--session-maintenance',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
13
36
|
const RESERVED_LANE_IDS = new Set([
|
|
14
37
|
'active',
|
|
15
38
|
'current',
|
|
@@ -114,6 +137,7 @@ export async function startProjectSession(options) {
|
|
|
114
137
|
status: 'active',
|
|
115
138
|
workingOn,
|
|
116
139
|
}, { current: sessionId });
|
|
140
|
+
const manifestRefresh = await refreshStateManifestSafely(normalized.rootDir);
|
|
117
141
|
const auditWarnings = await buildDocsReviewWarnings(normalized);
|
|
118
142
|
const metadataWarnings = collectLaneMetadataWarnings(existingLanes);
|
|
119
143
|
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
@@ -134,8 +158,10 @@ export async function startProjectSession(options) {
|
|
|
134
158
|
warnings: [
|
|
135
159
|
...metadataWarnings,
|
|
136
160
|
...overlapWarnings,
|
|
161
|
+
...manifestRefresh.warnings,
|
|
137
162
|
...auditWarnings,
|
|
138
163
|
],
|
|
164
|
+
manifest: manifestRefresh.manifest,
|
|
139
165
|
agentFileSync,
|
|
140
166
|
};
|
|
141
167
|
}
|
|
@@ -162,6 +188,14 @@ export async function closeProjectSession(options) {
|
|
|
162
188
|
const workflowSettings = await readWorkflowSettings(normalized.projectFilePath);
|
|
163
189
|
const sessionId = await resolveExistingSessionId(normalized, options);
|
|
164
190
|
const lanePaths = sessionId ? getLanePaths(normalized, sessionId) : normalized;
|
|
191
|
+
const documentMaintenance = normalizeDocumentMaintenanceCheckpoint(options?.documentMaintenance);
|
|
192
|
+
const docsUpdate = await planDocsUpdateSafely({
|
|
193
|
+
rootDir: normalized.rootDir,
|
|
194
|
+
cwd: normalized.cwd,
|
|
195
|
+
sessionId,
|
|
196
|
+
// Test injection seam; production callers should let close-session use the default planner.
|
|
197
|
+
planner: options?.docsUpdatePlanner,
|
|
198
|
+
});
|
|
165
199
|
|
|
166
200
|
if (!(await fileExists(lanePaths.wipFilePath))) {
|
|
167
201
|
throw new Error(
|
|
@@ -205,6 +239,7 @@ export async function closeProjectSession(options) {
|
|
|
205
239
|
completed,
|
|
206
240
|
decisions,
|
|
207
241
|
models: models.length > 0 ? models : ['Not recorded.'],
|
|
242
|
+
documentMaintenance,
|
|
208
243
|
blockers,
|
|
209
244
|
nextSteps,
|
|
210
245
|
}),
|
|
@@ -241,6 +276,7 @@ export async function closeProjectSession(options) {
|
|
|
241
276
|
);
|
|
242
277
|
|
|
243
278
|
await writeFile(normalized.claudePath, updatedClaude, 'utf8');
|
|
279
|
+
const manifestRefresh = await refreshStateManifestSafely(normalized.rootDir);
|
|
244
280
|
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
245
281
|
rootDir: normalized.rootDir,
|
|
246
282
|
toolingRootDir: normalized.toolingRootDir,
|
|
@@ -256,7 +292,14 @@ export async function closeProjectSession(options) {
|
|
|
256
292
|
sessionRelativePath,
|
|
257
293
|
sessionDate: activeSession.sessionDate,
|
|
258
294
|
sessionNumber: activeSession.sessionNumber,
|
|
295
|
+
documentMaintenance,
|
|
296
|
+
docsUpdatePlan: docsUpdate.plan,
|
|
259
297
|
workflowGuidance: buildCloseSessionGuidance(workflowSettings),
|
|
298
|
+
warnings: [
|
|
299
|
+
...docsUpdate.warnings,
|
|
300
|
+
...manifestRefresh.warnings,
|
|
301
|
+
],
|
|
302
|
+
manifest: manifestRefresh.manifest,
|
|
260
303
|
agentFileSync,
|
|
261
304
|
removedScratchFiles: [
|
|
262
305
|
lanePaths.wipFilePath,
|
|
@@ -265,6 +308,46 @@ export async function closeProjectSession(options) {
|
|
|
265
308
|
};
|
|
266
309
|
}
|
|
267
310
|
|
|
311
|
+
function normalizeDocumentMaintenanceCheckpoint(value) {
|
|
312
|
+
const source = value && typeof value === 'object' && !Array.isArray(value)
|
|
313
|
+
? value
|
|
314
|
+
: {};
|
|
315
|
+
const missing = [];
|
|
316
|
+
const invalid = [];
|
|
317
|
+
const checkpoint = {};
|
|
318
|
+
|
|
319
|
+
for (const field of DOCUMENT_MAINTENANCE_FIELDS) {
|
|
320
|
+
const normalized = normalizeOptionalString(source[field.key]);
|
|
321
|
+
if (!normalized) {
|
|
322
|
+
missing.push(`${field.flag} <updated|not-needed|deferred>`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!DOCUMENT_MAINTENANCE_STATUSES.has(normalized)) {
|
|
327
|
+
invalid.push(`${field.flag}=${normalized}`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
checkpoint[field.key] = normalized;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (missing.length > 0 || invalid.length > 0) {
|
|
335
|
+
const details = [
|
|
336
|
+
missing.length > 0
|
|
337
|
+
? `Missing document-maintenance checkpoint status: ${missing.join(', ')}.`
|
|
338
|
+
: null,
|
|
339
|
+
invalid.length > 0
|
|
340
|
+
? `Invalid document-maintenance checkpoint status: ${invalid.join(', ')}. Allowed values: updated, not-needed, deferred.`
|
|
341
|
+
: null,
|
|
342
|
+
'The package validates the close-session checkpoint, but the closer remains responsible for semantic doc updates.',
|
|
343
|
+
].filter(Boolean);
|
|
344
|
+
|
|
345
|
+
throw new Error(details.join(' '));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return checkpoint;
|
|
349
|
+
}
|
|
350
|
+
|
|
268
351
|
export async function listProjectSessions(options = {}) {
|
|
269
352
|
const normalized = normalizeSessionPaths(options);
|
|
270
353
|
await ensureInitializedProjectMemory(normalized.rootDir, { allowMissingProjectFile: true });
|
|
@@ -300,6 +383,7 @@ export async function switchProjectSession(options = {}) {
|
|
|
300
383
|
),
|
|
301
384
|
'utf8',
|
|
302
385
|
);
|
|
386
|
+
const manifestRefresh = await refreshStateManifestSafely(normalized.rootDir);
|
|
303
387
|
const agentFileSync = await syncAgentInstructionFilesSafely({
|
|
304
388
|
rootDir: normalized.rootDir,
|
|
305
389
|
toolingRootDir: normalized.toolingRootDir,
|
|
@@ -310,10 +394,50 @@ export async function switchProjectSession(options = {}) {
|
|
|
310
394
|
claudePath: normalized.claudePath,
|
|
311
395
|
current: sessionId,
|
|
312
396
|
lanes,
|
|
397
|
+
warnings: manifestRefresh.warnings,
|
|
398
|
+
manifest: manifestRefresh.manifest,
|
|
313
399
|
agentFileSync,
|
|
314
400
|
};
|
|
315
401
|
}
|
|
316
402
|
|
|
403
|
+
async function refreshStateManifestSafely(rootDir) {
|
|
404
|
+
try {
|
|
405
|
+
const manifest = await writeStateManifest(rootDir);
|
|
406
|
+
return {
|
|
407
|
+
manifest,
|
|
408
|
+
warnings: [],
|
|
409
|
+
};
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return {
|
|
412
|
+
manifest: null,
|
|
413
|
+
warnings: [
|
|
414
|
+
`State manifest refresh skipped: ${error instanceof Error ? error.message : String(error)}`,
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function planDocsUpdateSafely(options) {
|
|
421
|
+
try {
|
|
422
|
+
const planner = typeof options.planner === 'function' ? options.planner : planDocsUpdate;
|
|
423
|
+
return {
|
|
424
|
+
plan: await planner({
|
|
425
|
+
rootDir: options.rootDir,
|
|
426
|
+
cwd: options.cwd,
|
|
427
|
+
sessionId: options.sessionId,
|
|
428
|
+
}),
|
|
429
|
+
warnings: [],
|
|
430
|
+
};
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return {
|
|
433
|
+
plan: null,
|
|
434
|
+
warnings: [
|
|
435
|
+
`Docs-update plan skipped: ${error instanceof Error ? error.message : String(error)}`,
|
|
436
|
+
],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
317
441
|
async function syncAgentInstructionFilesSafely(options) {
|
|
318
442
|
try {
|
|
319
443
|
return await syncAgentInstructionFiles(options);
|
|
@@ -364,6 +488,7 @@ function normalizeSessionPaths(options) {
|
|
|
364
488
|
: cwd;
|
|
365
489
|
|
|
366
490
|
return {
|
|
491
|
+
cwd,
|
|
367
492
|
rootDir,
|
|
368
493
|
toolingRootDir,
|
|
369
494
|
projectFilePath: path.resolve(rootDir, 'project.yaml'),
|
|
@@ -918,6 +1043,9 @@ ${renderBulletList(options.decisions, 'No new decisions were logged in this sess
|
|
|
918
1043
|
## Models used
|
|
919
1044
|
${renderBulletList(options.models)}
|
|
920
1045
|
|
|
1046
|
+
## Document maintenance checkpoint
|
|
1047
|
+
${renderDocumentMaintenanceCheckpoint(options.documentMaintenance)}
|
|
1048
|
+
|
|
921
1049
|
## Blockers / open questions
|
|
922
1050
|
${renderBulletList(options.blockers, 'No blocker remains.')}
|
|
923
1051
|
|
|
@@ -926,6 +1054,12 @@ ${renderOrderedList(options.nextSteps)}
|
|
|
926
1054
|
`;
|
|
927
1055
|
}
|
|
928
1056
|
|
|
1057
|
+
function renderDocumentMaintenanceCheckpoint(documentMaintenance) {
|
|
1058
|
+
return DOCUMENT_MAINTENANCE_FIELDS
|
|
1059
|
+
.map((field) => `- ${field.label}: ${documentMaintenance[field.key]}`)
|
|
1060
|
+
.join('\n');
|
|
1061
|
+
}
|
|
1062
|
+
|
|
929
1063
|
function renderBulletList(items, fallback) {
|
|
930
1064
|
if (items.length === 0) {
|
|
931
1065
|
return `- ${fallback}`;
|
package/src/status.js
CHANGED
|
@@ -5,6 +5,7 @@ import { sha256Text } from './hash.js';
|
|
|
5
5
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
6
6
|
import { listProjectSessions } from './session.js';
|
|
7
7
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
8
|
+
import { scanProjectMemory } from './project-memory.js';
|
|
8
9
|
|
|
9
10
|
export async function getProjectStatus(options = {}) {
|
|
10
11
|
const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
|
|
@@ -18,11 +19,12 @@ export async function getProjectStatus(options = {}) {
|
|
|
18
19
|
cwd,
|
|
19
20
|
...(options.packageVersion ? { packageVersion: options.packageVersion } : {}),
|
|
20
21
|
});
|
|
21
|
-
const [project, sessions, docsReview, agentFiles] = await Promise.all([
|
|
22
|
+
const [project, sessions, docsReview, agentFiles, projectMemory] = await Promise.all([
|
|
22
23
|
readProjectSummary(compatibility.projectFilePath),
|
|
23
24
|
readSessionsStatus(rootDir),
|
|
24
25
|
readDocsReviewStatus(rootDir),
|
|
25
26
|
readAgentFileStatus(rootDir, toolingRootDir),
|
|
27
|
+
readProjectMemoryStatus(rootDir),
|
|
26
28
|
]);
|
|
27
29
|
|
|
28
30
|
return {
|
|
@@ -39,12 +41,14 @@ export async function getProjectStatus(options = {}) {
|
|
|
39
41
|
sessions,
|
|
40
42
|
docsReview,
|
|
41
43
|
agentFiles,
|
|
44
|
+
projectMemory,
|
|
42
45
|
recommendations: buildRecommendations({
|
|
43
46
|
project,
|
|
44
47
|
compatibility,
|
|
45
48
|
sessions,
|
|
46
49
|
docsReview,
|
|
47
50
|
agentFiles,
|
|
51
|
+
projectMemory,
|
|
48
52
|
}),
|
|
49
53
|
};
|
|
50
54
|
}
|
|
@@ -99,6 +103,9 @@ export function renderStatusText(status) {
|
|
|
99
103
|
? [`- Output drift: ${status.docsReview.outputDrift.reasons.join('; ')}`]
|
|
100
104
|
: []),
|
|
101
105
|
'',
|
|
106
|
+
'Project memory:',
|
|
107
|
+
...formatProjectMemoryStatus(status.projectMemory),
|
|
108
|
+
'',
|
|
102
109
|
'Agent files:',
|
|
103
110
|
...formatAgentFileStatus(status.agentFiles),
|
|
104
111
|
);
|
|
@@ -125,6 +132,7 @@ export function toStatusJson(status) {
|
|
|
125
132
|
sessions: status.sessions,
|
|
126
133
|
docsReview: status.docsReview,
|
|
127
134
|
agentFiles: status.agentFiles,
|
|
135
|
+
projectMemory: status.projectMemory,
|
|
128
136
|
recommendations: status.recommendations,
|
|
129
137
|
};
|
|
130
138
|
}
|
|
@@ -297,7 +305,48 @@ async function readAgentFileStatus(rootDir, toolingRootDir) {
|
|
|
297
305
|
}
|
|
298
306
|
}
|
|
299
307
|
|
|
300
|
-
function
|
|
308
|
+
async function readProjectMemoryStatus(rootDir) {
|
|
309
|
+
try {
|
|
310
|
+
const scan = await scanProjectMemory(rootDir);
|
|
311
|
+
const architectureDocs = scan.documents.filter((document) => document.kind === 'architecture');
|
|
312
|
+
const architectureWarnings = scan.warnings
|
|
313
|
+
.filter((warning) => warning.path?.startsWith('architecture/'))
|
|
314
|
+
.map((warning) => ({
|
|
315
|
+
path: warning.path,
|
|
316
|
+
code: warning.code,
|
|
317
|
+
message: warning.message,
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
status: scan.errors.length > 0 ? 'errors' : 'ok',
|
|
322
|
+
architectureDocCount: architectureDocs.length,
|
|
323
|
+
architectureWarningCount: architectureWarnings.length,
|
|
324
|
+
warningSample: architectureWarnings.slice(0, 10),
|
|
325
|
+
truncatedWarningCount: Math.max(0, architectureWarnings.length - 10),
|
|
326
|
+
errorCount: scan.errors.length,
|
|
327
|
+
errorSample: scan.errors.slice(0, 10).map((error) => ({
|
|
328
|
+
path: error.path,
|
|
329
|
+
code: error.code,
|
|
330
|
+
message: error.message,
|
|
331
|
+
})),
|
|
332
|
+
truncatedErrorCount: Math.max(0, scan.errors.length - 10),
|
|
333
|
+
};
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return {
|
|
336
|
+
status: 'unavailable',
|
|
337
|
+
architectureDocCount: 0,
|
|
338
|
+
architectureWarningCount: 0,
|
|
339
|
+
warningSample: [],
|
|
340
|
+
truncatedWarningCount: 0,
|
|
341
|
+
errorCount: 0,
|
|
342
|
+
errorSample: [],
|
|
343
|
+
truncatedErrorCount: 0,
|
|
344
|
+
errorMessage: error instanceof Error ? error.message : 'Unable to scan project memory.',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildRecommendations({ project, compatibility, sessions, docsReview, agentFiles, projectMemory }) {
|
|
301
350
|
const recommendations = [];
|
|
302
351
|
if (project.status === 'unreadable') {
|
|
303
352
|
recommendations.push('vibecompass init --guided');
|
|
@@ -320,6 +369,16 @@ function buildRecommendations({ project, compatibility, sessions, docsReview, ag
|
|
|
320
369
|
recommendations.push('vibecompass sync-agents --dry-run');
|
|
321
370
|
}
|
|
322
371
|
|
|
372
|
+
if (projectMemory.status === 'errors') {
|
|
373
|
+
recommendations.push('vibecompass status --json');
|
|
374
|
+
} else if (projectMemory.architectureWarningCount > 0) {
|
|
375
|
+
recommendations.push(
|
|
376
|
+
sessions.status === 'ok' && sessions.current
|
|
377
|
+
? `vibecompass docs-update --session ${sessions.current}`
|
|
378
|
+
: 'vibecompass docs-review --guided',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
323
382
|
if (sessions.status === 'ok' && sessions.lanes.length === 0) {
|
|
324
383
|
recommendations.push('vibecompass start-session --id LANE_ID --working-on "TASK"');
|
|
325
384
|
} else if (sessions.status === 'ok' && sessions.lanes.length >= 2) {
|
|
@@ -334,6 +393,8 @@ function hasStatusDrift(status) {
|
|
|
334
393
|
status.compatibility.warnings.length > 0 ||
|
|
335
394
|
status.docsReview.status !== 'completed' ||
|
|
336
395
|
Boolean(status.docsReview.outputDrift?.hasDrift) ||
|
|
396
|
+
status.projectMemory.status !== 'ok' ||
|
|
397
|
+
status.projectMemory.architectureWarningCount > 0 ||
|
|
337
398
|
status.agentFiles.status !== 'ok' ||
|
|
338
399
|
status.agentFiles.results.some((item) => item.changed || item.warning)
|
|
339
400
|
);
|
|
@@ -390,6 +451,34 @@ function formatDocsReviewStatus(docsReview) {
|
|
|
390
451
|
return `${docsReview.status}${docsReview.completedAt ? ` at ${docsReview.completedAt}` : ''}`;
|
|
391
452
|
}
|
|
392
453
|
|
|
454
|
+
function formatProjectMemoryStatus(projectMemory) {
|
|
455
|
+
if (projectMemory.status === 'unavailable') {
|
|
456
|
+
return [`- unavailable (${projectMemory.errorMessage})`];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const lines = [
|
|
460
|
+
`- Architecture docs: ${projectMemory.architectureDocCount}`,
|
|
461
|
+
`- Architecture warnings: ${projectMemory.architectureWarningCount}`,
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
for (const warning of projectMemory.warningSample) {
|
|
465
|
+
lines.push(`- ${warning.path}: ${warning.code} — ${warning.message}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (projectMemory.truncatedWarningCount > 0) {
|
|
469
|
+
lines.push(`- ${projectMemory.truncatedWarningCount} more architecture warning(s) omitted; use --json for the full status sample.`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (projectMemory.errorCount > 0) {
|
|
473
|
+
lines.push(`- Scan errors: ${projectMemory.errorCount}`);
|
|
474
|
+
for (const error of projectMemory.errorSample) {
|
|
475
|
+
lines.push(`- ${error.path}: ${error.code} — ${error.message}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return lines;
|
|
480
|
+
}
|
|
481
|
+
|
|
393
482
|
function formatAgentFileStatus(agentFiles) {
|
|
394
483
|
if (agentFiles.status !== 'ok') {
|
|
395
484
|
return [`- unavailable (${agentFiles.errorMessage})`];
|
package/src/workflow.js
CHANGED
|
@@ -78,6 +78,7 @@ export function buildCloseSessionGuidance(workflowSettings) {
|
|
|
78
78
|
const guidance = [];
|
|
79
79
|
|
|
80
80
|
guidance.push('Reviewer handback stays in the selected sessions/active/<lane-id>/wip.md and handoff.md before close-session runs.');
|
|
81
|
+
guidance.push('Close-session requires document-maintenance checkpoint statuses for architecture docs, decision log, and session handoff/scratch: updated, not-needed, or deferred.');
|
|
81
82
|
|
|
82
83
|
if (workflowSettings.closeSession.refreshArchitectureDocs) {
|
|
83
84
|
guidance.push('Refresh any relevant architecture docs before finalizing the session.');
|
|
@@ -12,7 +12,8 @@ export const WORKFLOW_REGISTRY = [
|
|
|
12
12
|
{ trigger: 'planning mode', summary: 'optional prompt-level mode for scoping work before implementation' },
|
|
13
13
|
{ trigger: 'review handoff', summary: 'reviewer reads the selected lane handoff and appends findings' },
|
|
14
14
|
{ trigger: 'address review', summary: 'builder critically resolves reviewer feedback from the selected lane scratch files' },
|
|
15
|
-
{ trigger: '
|
|
15
|
+
{ trigger: 'docs update', summary: 'builder runs a targeted session-delta documentation maintenance plan with `vibecompass docs-update --session <lane-id>`' },
|
|
16
|
+
{ trigger: 'close session', summary: 'builder runs the close-out checklist and ends with `vibecompass close-session --session <lane-id>` plus required document-maintenance checkpoint statuses; `end session` and `vibecompass end-session` are accepted aliases' },
|
|
16
17
|
],
|
|
17
18
|
},
|
|
18
19
|
{
|