@vibecompass/vibecompass 0.14.0 → 0.16.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 +183 -0
- package/README.md +111 -8
- package/package.json +1 -1
- package/src/brief-keywords.js +203 -0
- package/src/brief-lifecycle.js +638 -0
- package/src/brief-render.js +341 -0
- package/src/brief-settings.js +160 -0
- package/src/brief.js +1557 -0
- package/src/cli.js +731 -7
- package/src/credential-store.js +856 -0
- package/src/decision-lineage.js +1717 -0
- package/src/docs-review.js +28 -4
- package/src/generators/agent-files/template.js +2 -1
- package/src/index.js +44 -0
- package/src/login.js +257 -0
- package/src/mode-transition.js +8 -2
- package/src/project-memory.js +30 -4
- package/src/read-model.js +92 -2
- package/src/scaffold.js +10 -2
- package/src/secret-prompt.js +64 -0
- package/src/session.js +30 -0
- package/src/status.js +54 -9
- package/src/sync.js +12 -4
package/src/scaffold.js
CHANGED
|
@@ -226,6 +226,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
226
226
|
- \`${rootRelativePath}/sessions/active/<lane-id>/session.yaml\` — lane metadata (including the recorded \`lane_marker\` when one exists)
|
|
227
227
|
- \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\` — lane-local builder scratchpad
|
|
228
228
|
- \`${rootRelativePath}/sessions/active/<lane-id>/handoff.md\` — lane-local builder/reviewer relay
|
|
229
|
+
- \`${rootRelativePath}/sessions/active/<lane-id>/brief.md\` — generated session brief (D-359, D-364): the memory this lane should read first, written by \`start-session\` and \`continue-session\` when \`brief.enabled: true\` (opt-in, D-368) or on request with \`vibecompass brief --write\`; never hand-edit
|
|
229
230
|
- \`.vibecompass-lane.yaml\` — worktree-local lane marker (D-280); lives outside the memory root, written only by \`vibecompass write-lane-marker\` or worktree provisioning, never synced
|
|
230
231
|
- \`${rootRelativePath}/decisions/INDEX.md\` — derived grouped decision index; refresh with \`vibecompass refresh-decision-index\` (D-283, structure-preserving) instead of hand-editing rows
|
|
231
232
|
|
|
@@ -233,6 +234,7 @@ This workspace uses VibeCompass project memory rooted at \`${rootRelativePath}\`
|
|
|
233
234
|
- VibeCompass active builder sessions are named lanes. Use one lane per active feature or workstream.
|
|
234
235
|
- \`vibecompass start-session\` requires \`--id <lane-id>\` so each active lane has a meaningful feature or workstream name.
|
|
235
236
|
- The active lane scratch files live under \`${rootRelativePath}/sessions/active/<lane-id>/\`.
|
|
237
|
+
- When \`${rootRelativePath}/project.yaml\` sets \`brief.enabled: true\` (opt-in, D-368), \`start-session\` and \`continue-session\` write the lane's generated \`brief.md\` (D-359, D-364): the task is the lane's working-on, the files its claims, and the features its feature slugs. A brief failure is reported as a warning or an incomplete brief and never fails the lifecycle command. \`--no-brief\` skips it for one command, \`vibecompass brief --session <lane-id> --write\` writes one on request whatever the setting, and \`brief.exclude\` lists root-relative globs the brief must never read.
|
|
236
238
|
- Lane selection follows D-277: an explicit \`--session\` wins, then the nearest worktree lane marker (\`.vibecompass-lane.yaml\`, walking up from cwd), then the single active lane. With two or more active lanes there is no implicit current-lane fallback.
|
|
237
239
|
- \`${rootRelativePath}/sessions/active/index.yaml\` is the lane inventory; its \`current\` pointer and the tool-specific Current session block are human-readable continuity summaries, not the lane-selection source of truth.
|
|
238
240
|
- An active lane directory is an open session, not an unclosed crash (D-353). Resume it with \`vibecompass continue-session [<lane-id>]\`: the lane keeps its opening date and session number; the command records \`resumed_at\`/\`resume_count\` in \`session.yaml\`, appends a dated \`Resumed\` line to \`wip.md\`, moves the index \`current\` hint and the Current session block to the lane, and re-emits runtime, git-binding, and staleness warnings. \`start-session --id <existing-lane>\` fails closed and names \`continue-session\`.
|
|
@@ -276,7 +278,8 @@ ${renderWorkflowDefaults(workflow)}
|
|
|
276
278
|
3. Inventory active lanes with \`vibecompass list-sessions\` (or read \`${rootRelativePath}/sessions/active/index.yaml\`), then follow the "Open, resume, or choose" protocol below to decide between opening a new lane and resuming one; select a lane from an explicit \`--session\`, the nearest worktree lane marker, or the single active lane (D-277).
|
|
277
279
|
4. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/wip.md\`.
|
|
278
280
|
5. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/handoff.md\`.
|
|
279
|
-
6.
|
|
281
|
+
6. If present, read \`${rootRelativePath}/sessions/active/<lane-id>/brief.md\` before planning: the generated session brief selects the architecture docs, decisions (with their declared successors), and notes for the lane's task. Its status line says whether it is complete, partial, incomplete (read the listed required reads first), or no-match; \`vibecompass brief --check\` reports whether it is stale.
|
|
282
|
+
7. Read the relevant docs under \`${rootRelativePath}/architecture/\` and \`${rootRelativePath}/decisions/\`, starting from the brief's units and follow-up reads.
|
|
280
283
|
|
|
281
284
|
## Open, resume, or choose (D-353)
|
|
282
285
|
Both \`start session\` and \`continue session\` begin with the active-lane inventory. Then:
|
|
@@ -284,7 +287,7 @@ Both \`start session\` and \`continue session\` begin with the active-lane inven
|
|
|
284
287
|
- **Exactly one active lane, or a cwd bound to a lane by a worktree marker:** resume it with \`vibecompass continue-session\` without asking. Open a second lane only when the user is clearly asking for separate new work; never re-open a lane that already exists.
|
|
285
288
|
- **Two or more active lanes, no marker, no lane named by the user:** do not guess. Present the inventory (lane id, working-on, opened date, resume count, branch/worktree, last log line) and ask which lane to continue or whether to open a new one. The index \`current\` pointer and the Current session block are continuity hints to mention, never defaults to adopt silently. Resume with \`vibecompass continue-session <lane-id>\` once the user answers (\`continue session <lane-id>\`).
|
|
286
289
|
- **Lane named by the user:** \`continue session <lane-id>\` resumes that lane; an unknown id fails closed with the inventory.
|
|
287
|
-
- After resuming, read the lane's \`wip.md\` (latest \`## Log\` entries and \`## Reviewer input needed\`), \`handoff.md\`, and the latest finalized note; treat resume-staleness warnings (new decisions past the lane snapshot, stale base revisions, newer finalized notes touching the lane scope, claim overlap) as required reading before editing.
|
|
290
|
+
- After resuming, read the lane's \`wip.md\` (latest \`## Log\` entries and \`## Reviewer input needed\`), \`handoff.md\`, \`brief.md\` if present (with \`brief.enabled: true\`, continue-session keeps a current brief and regenerates a missing or stale one), and the latest finalized note; treat resume-staleness warnings (new decisions past the lane snapshot, stale base revisions, newer finalized notes touching the lane scope, claim overlap) as required reading before editing.
|
|
288
291
|
- Run recovery (reconstruct a missing finalized note from git history) only when the Current session block date is in the past *and* no active lane exists *and* no finalized note covers that date. An active lane with a past date is simply resumable.
|
|
289
292
|
|
|
290
293
|
## Builder workflow
|
|
@@ -302,6 +305,8 @@ Session lane: <lane-id>
|
|
|
302
305
|
|
|
303
306
|
## Reviewer input needed
|
|
304
307
|
|
|
308
|
+
## Context used
|
|
309
|
+
|
|
305
310
|
## Review log
|
|
306
311
|
\`\`\`
|
|
307
312
|
|
|
@@ -330,6 +335,8 @@ Session lane: <lane-id>
|
|
|
330
335
|
During the session:
|
|
331
336
|
- append short summaries to \`wip.md\` after meaningful exchanges
|
|
332
337
|
- keep \`handoff.md\` current after substantive work blocks
|
|
338
|
+
- optionally list under \`wip.md\` \`## Context used\` the docs and decisions the lane actually relied on (path or D-number, and what for), so the brief's usefulness can be audited
|
|
339
|
+
- refresh the lane brief with \`vibecompass brief --write\` after the lane's scope changes; \`vibecompass brief --check\` reports whether it is stale
|
|
333
340
|
- run \`eval "$(vibecompass lane-env)"\` in a lane shell before starting dev servers or build tools so the lane's assigned port and temp dir are used (D-282); do not hardcode ports in lane work
|
|
334
341
|
- run \`vibecompass docs-update --session <lane-id>\` whenever you need an ad hoc targeted documentation-maintenance plan for the current session delta
|
|
335
342
|
- after substantive feature work, confirm affected architecture docs and decisions still match the implementation; if not, update them while the context is fresh — fold the changes into the doc's current-state sections (rewrite in place; no dated "update" sections, lane names in headings, or completed-task chronology; D-292)
|
|
@@ -588,6 +595,7 @@ Finalized session notes for ${projectConfig.name} live here.
|
|
|
588
595
|
- \`active/<lane-id>/session.yaml\` — lane metadata, including the recorded \`lane_marker\` when one exists
|
|
589
596
|
- \`active/<lane-id>/wip.md\` — builder scratchpad during an active lane
|
|
590
597
|
- \`active/<lane-id>/handoff.md\` — reviewer/builder baton-pass during an active lane
|
|
598
|
+
- \`active/<lane-id>/brief.md\` — generated session brief for the lane (D-359, D-364); regenerated, never hand-edited
|
|
591
599
|
|
|
592
600
|
Those scratch files are session-scoped working artifacts, not finalized history.
|
|
593
601
|
`;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hidden-input prompt for secrets (D-355). Typed characters are never echoed:
|
|
6
|
+
* readline runs in terminal mode against a muted output while the visible
|
|
7
|
+
* prompt text is written to the real stdout. A `runtime.prompt` adapter
|
|
8
|
+
* (tests, embedders) receives `{ type: 'password', message }` instead.
|
|
9
|
+
*/
|
|
10
|
+
export function canPromptSecret(io = {}, runtime = {}) {
|
|
11
|
+
if (typeof runtime.prompt === 'function') {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
const input = runtime.stdin ?? io.stdin ?? process.stdin;
|
|
15
|
+
return Boolean(input?.isTTY);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function promptSecret(message, io = {}, runtime = {}) {
|
|
19
|
+
if (typeof runtime.prompt === 'function') {
|
|
20
|
+
return normalizeSecret(await runtime.prompt({ type: 'password', message }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const input = runtime.stdin ?? io.stdin ?? process.stdin;
|
|
24
|
+
const output = io.stdout ?? process.stdout;
|
|
25
|
+
if (!input?.isTTY) {
|
|
26
|
+
throw new Error('Secret prompts require an interactive terminal. Pipe the value with --token-stdin instead.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
output.write(`${message}: `);
|
|
30
|
+
const muted = new Writable({
|
|
31
|
+
write(_chunk, _encoding, callback) {
|
|
32
|
+
callback();
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const rl = createInterface({ input, output: muted, terminal: true });
|
|
36
|
+
try {
|
|
37
|
+
return normalizeSecret(await rl.question(''));
|
|
38
|
+
} finally {
|
|
39
|
+
rl.close();
|
|
40
|
+
output.write('\n');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Reads one secret from stdin: the first non-empty line, trimmed. */
|
|
45
|
+
export async function readSecretFromStdin(io = {}, runtime = {}) {
|
|
46
|
+
const input = runtime.stdin ?? io.stdin ?? process.stdin;
|
|
47
|
+
if (typeof input?.setEncoding === 'function') {
|
|
48
|
+
input.setEncoding('utf8');
|
|
49
|
+
}
|
|
50
|
+
let data = '';
|
|
51
|
+
for await (const chunk of input) {
|
|
52
|
+
data += chunk;
|
|
53
|
+
}
|
|
54
|
+
const firstLine = data
|
|
55
|
+
.split(/\r?\n/)
|
|
56
|
+
.map((line) => line.trim())
|
|
57
|
+
.find((line) => line !== '');
|
|
58
|
+
return firstLine ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeSecret(value) {
|
|
62
|
+
const trimmed = String(value ?? '').trim();
|
|
63
|
+
return trimmed === '' ? null : trimmed;
|
|
64
|
+
}
|
package/src/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { access, mkdir, readFile, readdir, realpath, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { refreshLaneBriefForLifecycle } from './brief-lifecycle.js';
|
|
4
5
|
import { scanProjectMemory } from './project-memory.js';
|
|
5
6
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
6
7
|
import { buildCloseSessionGuidance, resolveWorkflowSettings } from './workflow.js';
|
|
@@ -398,6 +399,16 @@ async function startProjectSessionLocked(normalized, options, markerContext) {
|
|
|
398
399
|
rootDir: normalized.rootDir,
|
|
399
400
|
toolingRootDir: normalized.toolingRootDir,
|
|
400
401
|
});
|
|
402
|
+
// D-359/D-364/D-368: the opt-in lane brief is written only after every
|
|
403
|
+
// lifecycle write succeeded, still inside the memory-root lock; it never
|
|
404
|
+
// throws, so a brief failure cannot overturn the start.
|
|
405
|
+
const brief = await refreshLaneBriefForLifecycle({
|
|
406
|
+
trigger: 'start',
|
|
407
|
+
rootDir: normalized.rootDir,
|
|
408
|
+
laneId: sessionId,
|
|
409
|
+
disabled: options?.brief === false,
|
|
410
|
+
buildBrief: options?.briefBuilder,
|
|
411
|
+
});
|
|
401
412
|
|
|
402
413
|
return {
|
|
403
414
|
rootDir: normalized.rootDir,
|
|
@@ -420,7 +431,9 @@ async function startProjectSessionLocked(normalized, options, markerContext) {
|
|
|
420
431
|
...(gitBinding?.warnings ?? []),
|
|
421
432
|
...manifestRefresh.warnings,
|
|
422
433
|
...auditWarnings,
|
|
434
|
+
...brief.warnings,
|
|
423
435
|
],
|
|
436
|
+
brief,
|
|
424
437
|
...(gitBinding
|
|
425
438
|
? {
|
|
426
439
|
gitBinding: {
|
|
@@ -1145,6 +1158,18 @@ async function continueProjectSessionLocked(normalized, options, markerContext)
|
|
|
1145
1158
|
rootDir: normalized.rootDir,
|
|
1146
1159
|
toolingRootDir: normalized.toolingRootDir,
|
|
1147
1160
|
});
|
|
1161
|
+
// D-359/D-364: keep a current lane brief, regenerate a missing or stale
|
|
1162
|
+
// one. `--source-root` (D-255 dogfood) is read only when named here; it is
|
|
1163
|
+
// never inferred from the existing brief's header.
|
|
1164
|
+
const brief = await refreshLaneBriefForLifecycle({
|
|
1165
|
+
trigger: 'continue',
|
|
1166
|
+
rootDir: normalized.rootDir,
|
|
1167
|
+
laneId: lane.id,
|
|
1168
|
+
disabled: options?.brief === false,
|
|
1169
|
+
sourceRootDir: options?.sourceRootDir ? path.resolve(normalized.cwd, options.sourceRootDir) : undefined,
|
|
1170
|
+
sourceSessionId: normalizeOptionalString(options?.sourceSessionId) ?? undefined,
|
|
1171
|
+
buildBrief: options?.briefBuilder,
|
|
1172
|
+
});
|
|
1148
1173
|
const finalizedSessions = await listFinalizedSessions(normalized.sessionsDir);
|
|
1149
1174
|
const latestFinalized = finalizedSessions.length > 0 ? finalizedSessions[finalizedSessions.length - 1] : null;
|
|
1150
1175
|
// The returned inventory is the post-resume state (what list-sessions shows
|
|
@@ -1181,6 +1206,7 @@ async function continueProjectSessionLocked(normalized, options, markerContext)
|
|
|
1181
1206
|
otherLanes: inventoryAfter.lanes.filter((item) => item.id !== lane.id),
|
|
1182
1207
|
inventory: inventoryAfter,
|
|
1183
1208
|
docsUpdatePlan: docsUpdate.plan,
|
|
1209
|
+
brief,
|
|
1184
1210
|
warnings: [
|
|
1185
1211
|
...markerContext.warnings,
|
|
1186
1212
|
...selection.warnings,
|
|
@@ -1196,6 +1222,7 @@ async function continueProjectSessionLocked(normalized, options, markerContext)
|
|
|
1196
1222
|
}),
|
|
1197
1223
|
...docsUpdate.warnings,
|
|
1198
1224
|
...manifestRefresh.warnings,
|
|
1225
|
+
...brief.warnings,
|
|
1199
1226
|
],
|
|
1200
1227
|
manifest: manifestRefresh.manifest,
|
|
1201
1228
|
agentFileSync,
|
|
@@ -2279,6 +2306,9 @@ ${options.workingOn}
|
|
|
2279
2306
|
## Reviewer input needed
|
|
2280
2307
|
- None yet.
|
|
2281
2308
|
|
|
2309
|
+
## Context used
|
|
2310
|
+
- Optional: the docs and decisions this lane actually relied on (path or D-number, and what for) — an audit trail for the session brief; list what you used, not everything the brief showed.
|
|
2311
|
+
|
|
2282
2312
|
## Review log
|
|
2283
2313
|
`;
|
|
2284
2314
|
}
|
package/src/status.js
CHANGED
|
@@ -5,6 +5,7 @@ import { sha256Text } from './hash.js';
|
|
|
5
5
|
import { resolveLaneMarkerContext } from './lane-marker.js';
|
|
6
6
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
7
7
|
import { resolveSyncBinding } from './sync-binding.js';
|
|
8
|
+
import { describeCredentialSource, resolveSyncCredential } from './credential-store.js';
|
|
8
9
|
import { inferToolingRootForMemoryRoot, listProjectSessions } from './session.js';
|
|
9
10
|
import { syncAgentInstructionFiles } from './generators/agent-files/index.js';
|
|
10
11
|
import { scanProjectMemory } from './project-memory.js';
|
|
@@ -68,6 +69,7 @@ export async function getProjectStatus(options = {}) {
|
|
|
68
69
|
docsReview,
|
|
69
70
|
agentFiles,
|
|
70
71
|
projectMemory,
|
|
72
|
+
hostedSync,
|
|
71
73
|
}),
|
|
72
74
|
};
|
|
73
75
|
}
|
|
@@ -125,6 +127,7 @@ export function renderStatusText(status) {
|
|
|
125
127
|
: status.hostedSync?.status === 'unreachable'
|
|
126
128
|
? [`Hosted mode check: unreachable (${status.hostedSync.detail}).`]
|
|
127
129
|
: []),
|
|
130
|
+
...formatHostedSyncLines(status.hostedSync),
|
|
128
131
|
`Root: ${status.rootDir}`,
|
|
129
132
|
`Repos: ${status.project.repos.length > 0 ? status.project.repos.map((repo) => repo.id).join(', ') : 'None recorded'}`,
|
|
130
133
|
`Active lanes: ${formatActiveLanes(status.sessions)}`,
|
|
@@ -207,16 +210,36 @@ async function readHostedModeCheck(projectFilePath, localMode, options = {}) {
|
|
|
207
210
|
}
|
|
208
211
|
if (!binding) return { status: 'not-configured' };
|
|
209
212
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
:
|
|
213
|
+
const bindingSummary = {
|
|
214
|
+
apiUrl: binding.apiUrl,
|
|
215
|
+
projectId: binding.projectId,
|
|
216
|
+
target: binding.target ?? null,
|
|
217
|
+
credentialEnvVar: binding.credentialEnvVar,
|
|
218
|
+
};
|
|
219
|
+
// D-355: report only the source of the credential (env / keychain / file /
|
|
220
|
+
// missing), never its value.
|
|
221
|
+
const resolution = await resolveSyncCredential(binding, {
|
|
222
|
+
env: options.env ?? process.env,
|
|
223
|
+
...(options.credentialStore ?? {}),
|
|
224
|
+
});
|
|
225
|
+
const credential = resolution.credential;
|
|
226
|
+
const credentialSource = credential ? resolution.source : 'missing';
|
|
227
|
+
const credentialSourceLabel = credential ? describeCredentialSource(resolution) : 'missing';
|
|
214
228
|
if (!credential) {
|
|
215
|
-
return {
|
|
229
|
+
return {
|
|
230
|
+
status: 'no-credential',
|
|
231
|
+
credentialEnvVar: binding.credentialEnvVar,
|
|
232
|
+
credentialSource,
|
|
233
|
+
credentialSourceLabel,
|
|
234
|
+
...(resolution.detail ? { detail: resolution.detail } : {}),
|
|
235
|
+
binding: bindingSummary,
|
|
236
|
+
};
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
219
|
-
if (typeof fetchImpl !== 'function')
|
|
240
|
+
if (typeof fetchImpl !== 'function') {
|
|
241
|
+
return { status: 'not-configured', credentialSource, credentialSourceLabel, binding: bindingSummary };
|
|
242
|
+
}
|
|
220
243
|
|
|
221
244
|
const baseUrl = binding.apiUrl.endsWith('/') ? binding.apiUrl : `${binding.apiUrl}/`;
|
|
222
245
|
const endpoint = new URL(
|
|
@@ -234,7 +257,7 @@ async function readHostedModeCheck(projectFilePath, localMode, options = {}) {
|
|
|
234
257
|
signal: controller.signal,
|
|
235
258
|
});
|
|
236
259
|
if (!response.ok) {
|
|
237
|
-
return { status: 'unreachable', detail: `HTTP ${response.status}
|
|
260
|
+
return { status: 'unreachable', detail: `HTTP ${response.status}`, credentialSource, credentialSourceLabel, binding: bindingSummary };
|
|
238
261
|
}
|
|
239
262
|
const data = typeof response.json === 'function' ? await response.json() : {};
|
|
240
263
|
const hostedMode = typeof data.mode === 'string' ? data.mode : null;
|
|
@@ -243,9 +266,12 @@ async function readHostedModeCheck(projectFilePath, localMode, options = {}) {
|
|
|
243
266
|
hostedMode,
|
|
244
267
|
localMode,
|
|
245
268
|
mismatch: Boolean(hostedMode && localMode && hostedMode !== localMode),
|
|
269
|
+
credentialSource,
|
|
270
|
+
credentialSourceLabel,
|
|
271
|
+
binding: bindingSummary,
|
|
246
272
|
};
|
|
247
273
|
} catch (error) {
|
|
248
|
-
return { status: 'unreachable', detail: error?.message ?? 'network error' };
|
|
274
|
+
return { status: 'unreachable', detail: error?.message ?? 'network error', credentialSource, credentialSourceLabel, binding: bindingSummary };
|
|
249
275
|
} finally {
|
|
250
276
|
clearTimeout(timer);
|
|
251
277
|
}
|
|
@@ -460,13 +486,32 @@ async function readProjectMemoryStatus(rootDir) {
|
|
|
460
486
|
}
|
|
461
487
|
}
|
|
462
488
|
|
|
463
|
-
function
|
|
489
|
+
function formatHostedSyncLines(hostedSync) {
|
|
490
|
+
if (!hostedSync?.binding) {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
const { binding } = hostedSync;
|
|
494
|
+
const where = `${binding.apiUrl} (project ${binding.projectId}${binding.target ? `, target ${binding.target}` : ''})`;
|
|
495
|
+
const credential = hostedSync.credentialSource === 'missing'
|
|
496
|
+
? `missing${hostedSync.detail ? ` (${hostedSync.detail})` : ''} — run \`vibecompass login\` to sign in from this terminal`
|
|
497
|
+
: hostedSync.credentialSourceLabel ?? hostedSync.credentialSource;
|
|
498
|
+
return [
|
|
499
|
+
`Hosted sync: ${where}`,
|
|
500
|
+
`Sync credential: ${credential}`,
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function buildRecommendations({ project, compatibility, sessions, docsReview, agentFiles, projectMemory, hostedSync }) {
|
|
464
505
|
const recommendations = [];
|
|
465
506
|
if (project.status === 'unreadable') {
|
|
466
507
|
recommendations.push('vibecompass init --guided');
|
|
467
508
|
return recommendations;
|
|
468
509
|
}
|
|
469
510
|
|
|
511
|
+
if (hostedSync?.status === 'no-credential') {
|
|
512
|
+
recommendations.push('vibecompass login');
|
|
513
|
+
}
|
|
514
|
+
|
|
470
515
|
const packageStatus = compatibility.package.status;
|
|
471
516
|
const stateStatus = compatibility.state.status;
|
|
472
517
|
if (['legacy', 'behind', 'ahead', 'invalid'].includes(packageStatus) || ['missing', 'unreadable', 'unsupported'].includes(stateStatus)) {
|
package/src/sync.js
CHANGED
|
@@ -4,6 +4,11 @@ import { sha256Text } from './hash.js';
|
|
|
4
4
|
import { writeStateManifest } from './manifest.js';
|
|
5
5
|
import { withMemoryRootLock } from './serialization.js';
|
|
6
6
|
import { parseSimpleYaml } from './simple-yaml.js';
|
|
7
|
+
import {
|
|
8
|
+
credentialStoreOptionsFrom,
|
|
9
|
+
formatMissingCredentialError,
|
|
10
|
+
resolveSyncCredential,
|
|
11
|
+
} from './credential-store.js';
|
|
7
12
|
import {
|
|
8
13
|
buildSyncStateWithCursor,
|
|
9
14
|
readSyncCursor,
|
|
@@ -465,12 +470,14 @@ async function loadSyncContext(options, environment) {
|
|
|
465
470
|
throw new Error('Hosted sync commands require sync.api_url, sync.project_id, and sync.credential_env_var in project.yaml (or a named target under sync.targets).');
|
|
466
471
|
}
|
|
467
472
|
|
|
468
|
-
|
|
473
|
+
// D-355: env var override first, then the local credential store
|
|
474
|
+
// (keychain or per-user file). The resolved value is used only as the
|
|
475
|
+
// bearer credential and never written anywhere.
|
|
476
|
+
const resolution = await resolveSyncCredential(binding, credentialStoreOptionsFrom(environment));
|
|
477
|
+
const credential = resolution.credential;
|
|
469
478
|
if (!credential) {
|
|
470
479
|
throw new Error(
|
|
471
|
-
|
|
472
|
-
+ `re-export it (export ${binding.credentialEnvVar}="<sync token>") or persist it in your shell profile (~/.zshenv or ~/.bashrc). `
|
|
473
|
-
+ 'Lost the token? Rotate it on the hosted dashboard under Setup -> Hosted sync, then export the new value.',
|
|
480
|
+
formatMissingCredentialError(binding, { action: 'This hosted sync command', detail: resolution.detail }),
|
|
474
481
|
);
|
|
475
482
|
}
|
|
476
483
|
|
|
@@ -485,6 +492,7 @@ async function loadSyncContext(options, environment) {
|
|
|
485
492
|
manifestPath,
|
|
486
493
|
fetch: fetchImpl,
|
|
487
494
|
credential,
|
|
495
|
+
credentialSource: resolution.source,
|
|
488
496
|
apiUrl: binding.apiUrl,
|
|
489
497
|
projectId: binding.projectId,
|
|
490
498
|
syncTarget: binding.target,
|