wendkeep 0.76.2 → 0.76.4
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 +24 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/en/commands/changes-and-verification.md +12 -5
- package/docs/en/commands/context.md +43 -8
- package/docs/pt-BR/commands/changes-and-verification.md +12 -5
- package/docs/pt-BR/commands/context.md +42 -8
- package/hooks/active-context-store.mjs +232 -0
- package/hooks/change-core.mjs +35 -17
- package/hooks/obsidian-common.mjs +5 -2
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +6 -2
- package/src/active-context-runtime.mjs +131 -0
- package/src/change.mjs +39 -10
- package/src/context.mjs +276 -1
- package/src/spec.mjs +15 -2
- package/src/verify.mjs +15 -1
package/hooks/change-core.mjs
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, renameVaultPath,
|
|
11
11
|
unlinkVaultFile, writeVaultFileSync,
|
|
12
12
|
} from './vault-path-safety.mjs';
|
|
13
|
+
import {
|
|
14
|
+
clearActiveContextChange,
|
|
15
|
+
resolveActiveContext,
|
|
16
|
+
setActiveContextChange,
|
|
17
|
+
} from './active-context-store.mjs';
|
|
13
18
|
|
|
14
19
|
export const ARCHIVE_DIR = '_arquivo';
|
|
15
20
|
const POINTER = '.brain/CURRENT_CHANGE.md';
|
|
@@ -118,7 +123,14 @@ export function scaffoldPlaceholders(dir) {
|
|
|
118
123
|
return found;
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
export function activeChange(vaultBase) {
|
|
126
|
+
export function activeChange(vaultBase, { context } = {}) {
|
|
127
|
+
if (context) {
|
|
128
|
+
try { return String(resolveActiveContext(vaultBase, context).change_slug || '').trim(); }
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND') return '';
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
122
134
|
try {
|
|
123
135
|
const m = readFileSync(join(vaultBase, POINTER), 'utf8').match(/^change:\s*(.+)$/m);
|
|
124
136
|
return m ? m[1].trim() : '';
|
|
@@ -127,7 +139,8 @@ export function activeChange(vaultBase) {
|
|
|
127
139
|
}
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
export function setActiveChange(vaultBase, slug) {
|
|
142
|
+
export function setActiveChange(vaultBase, slug, { context } = {}) {
|
|
143
|
+
if (context) return setActiveContextChange(vaultBase, context, slug);
|
|
131
144
|
mkdirVaultPath(vaultBase, join(vaultBase, '.brain'), { label: 'raiz de controle da change' });
|
|
132
145
|
writeVaultFileSync(
|
|
133
146
|
vaultBase,
|
|
@@ -138,7 +151,8 @@ export function setActiveChange(vaultBase, slug) {
|
|
|
138
151
|
);
|
|
139
152
|
}
|
|
140
153
|
|
|
141
|
-
export function clearActiveChange(vaultBase) {
|
|
154
|
+
export function clearActiveChange(vaultBase, { context } = {}) {
|
|
155
|
+
if (context) return clearActiveContextChange(vaultBase, context);
|
|
142
156
|
const p = join(vaultBase, POINTER);
|
|
143
157
|
const checked = assertVaultPathSafe(vaultBase, p, {
|
|
144
158
|
expectedType: 'file', label: 'ponteiro CURRENT_CHANGE.md',
|
|
@@ -191,7 +205,9 @@ export function assertChangeScaffoldTargetsSafe(vaultBase, slug, {
|
|
|
191
205
|
return { dir, rel: changeDirRel(slug, vaultBase) };
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
export function newChange(vaultBase, slug, {
|
|
208
|
+
export function newChange(vaultBase, slug, {
|
|
209
|
+
sessionRel = '', dateStr, simple = false, guide = false, context,
|
|
210
|
+
}) {
|
|
195
211
|
const compact = simple || guide;
|
|
196
212
|
const loc = getLocale(vaultBase);
|
|
197
213
|
const { dir } = assertChangeScaffoldTargetsSafe(vaultBase, slug, { simple: compact });
|
|
@@ -218,14 +234,14 @@ export function newChange(vaultBase, slug, { sessionRel = '', dateStr, simple =
|
|
|
218
234
|
write('design.md', files.design);
|
|
219
235
|
}
|
|
220
236
|
if (!existed) captureSpecBaseline(vaultBase, dir);
|
|
221
|
-
setActiveChange(vaultBase, slug);
|
|
237
|
+
setActiveChange(vaultBase, slug, { context });
|
|
222
238
|
return { rel: changeDirRel(slug, vaultBase), created: !existed };
|
|
223
239
|
}
|
|
224
240
|
|
|
225
|
-
export function useChange(vaultBase, slug) {
|
|
241
|
+
export function useChange(vaultBase, slug, { context } = {}) {
|
|
226
242
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
227
243
|
if (!existsSync(join(dir, 'proposta.md'))) return { ok: false, error: `change aberta não encontrada: ${slug}` };
|
|
228
|
-
setActiveChange(vaultBase, slug);
|
|
244
|
+
setActiveChange(vaultBase, slug, { context });
|
|
229
245
|
return { ok: true, rel: changeDirRel(slug, vaultBase) };
|
|
230
246
|
}
|
|
231
247
|
|
|
@@ -314,8 +330,8 @@ export function listChanges(vaultBase) {
|
|
|
314
330
|
// comandos implícitos; provider/session nunca filtram a fila, para que outro agente possa assumir
|
|
315
331
|
// o trabalho. O hash leva o conteúdo inteiro de cada tarefas.md — não apenas as contagens — pois
|
|
316
332
|
// ele controla a reinjeção por sessão dos hooks.
|
|
317
|
-
export function allChangesState(vaultBase) {
|
|
318
|
-
const current = activeChange(vaultBase);
|
|
333
|
+
export function allChangesState(vaultBase, { context } = {}) {
|
|
334
|
+
const current = activeChange(vaultBase, { context });
|
|
319
335
|
const { active } = listChanges(vaultBase);
|
|
320
336
|
const chDir = getLocale(vaultBase).folders.changes;
|
|
321
337
|
const fingerprint = [`current:${current}`];
|
|
@@ -407,16 +423,16 @@ export function buildActiveChangeInjection(vaultBase, options = {}) {
|
|
|
407
423
|
return renderOpenChanges(allChangesState(vaultBase), options);
|
|
408
424
|
}
|
|
409
425
|
|
|
410
|
-
export function activeChangeLink(vaultBase) {
|
|
411
|
-
const slug = activeChange(vaultBase);
|
|
426
|
+
export function activeChangeLink(vaultBase, { context } = {}) {
|
|
427
|
+
const slug = activeChange(vaultBase, { context });
|
|
412
428
|
return slug ? `Change ativa: [[${getLocale(vaultBase).folders.changes}/${slug}/proposta]]` : '';
|
|
413
429
|
}
|
|
414
430
|
|
|
415
431
|
// --- Estado rápido do gate + sentinelas por sessão (0.31.0) --------------------
|
|
416
432
|
// Fonte única e barata (só leituras, tudo fail-open) do estado do gate, consumida pelos hooks
|
|
417
433
|
// de lifecycle (change-guard/change-nag/change-context) e pelo CLI. null sem change ativa.
|
|
418
|
-
export function quickGateState(vaultBase) {
|
|
419
|
-
const slug = activeChange(vaultBase);
|
|
434
|
+
export function quickGateState(vaultBase, { context } = {}) {
|
|
435
|
+
const slug = activeChange(vaultBase, { context });
|
|
420
436
|
if (!slug) return null;
|
|
421
437
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
422
438
|
let tarefasMd = '';
|
|
@@ -536,7 +552,9 @@ export function gateGreen() {
|
|
|
536
552
|
return { ok: true, failing: [] };
|
|
537
553
|
}
|
|
538
554
|
|
|
539
|
-
export function archiveChange(vaultBase, slug, {
|
|
555
|
+
export function archiveChange(vaultBase, slug, {
|
|
556
|
+
gate = gateGreen, dateStr, adrNum, adrFlags = {}, context,
|
|
557
|
+
}) {
|
|
540
558
|
const loc = getLocale(vaultBase);
|
|
541
559
|
const chDir = loc.folders.changes;
|
|
542
560
|
const src = join(vaultBase, chDir, slug);
|
|
@@ -656,7 +674,7 @@ Mudança ${changeWikilink} concluída e arquivada.${capLine}${reqLine}${forcedNo
|
|
|
656
674
|
|
|
657
675
|
// Only clear the pointer when the archived change IS the active one — archiving some other
|
|
658
676
|
// slug explicitly must not blank the pointer of a different, still-active change.
|
|
659
|
-
if (activeChange(vaultBase) === slug) clearActiveChange(vaultBase);
|
|
677
|
+
if (activeChange(vaultBase, { context }) === slug) clearActiveChange(vaultBase, { context });
|
|
660
678
|
return { ok: true, failing: [], archivedRel: destRel, adrRel: createAdr ? adrRel : '', promoted, specWarnings, linksRewritten };
|
|
661
679
|
}
|
|
662
680
|
|
|
@@ -849,7 +867,7 @@ export function relinkChanges(vaultBase, { apply = false } = {}) {
|
|
|
849
867
|
// Abandono (0.31.0): a saída legítima para uma change que não vai adiante — o que antes só o
|
|
850
868
|
// `archive --force` "resolvia", minting um ADR falso. Sem ADR, sem promoteSpecs (abandono não é
|
|
851
869
|
// decisão arquitetural nem promove contrato); move para _arquivo com sufixo -abandonada.
|
|
852
|
-
export function abandonChange(vaultBase, slug, { dateStr }) {
|
|
870
|
+
export function abandonChange(vaultBase, slug, { dateStr, context } = {}) {
|
|
853
871
|
const chDir = getLocale(vaultBase).folders.changes;
|
|
854
872
|
const src = join(vaultBase, chDir, slug);
|
|
855
873
|
const checkedProposal = assertVaultPathSafe(vaultBase, join(src, 'proposta.md'), {
|
|
@@ -882,6 +900,6 @@ export function abandonChange(vaultBase, slug, { dateStr }) {
|
|
|
882
900
|
} catch { /* proposta sem frontmatter — segue */ }
|
|
883
901
|
let linksRewritten = 0;
|
|
884
902
|
try { linksRewritten = rewriteChangeLinks(vaultBase, `${chDir}/${slug}`, destRel.replaceAll('\\', '/')); } catch { /* abandono já íntegro */ }
|
|
885
|
-
if (activeChange(vaultBase) === slug) clearActiveChange(vaultBase);
|
|
903
|
+
if (activeChange(vaultBase, { context }) === slug) clearActiveChange(vaultBase, { context });
|
|
886
904
|
return { ok: true, failing: [], archivedRel: destRel, linksRewritten };
|
|
887
905
|
}
|
|
@@ -276,9 +276,12 @@ export function readSessionRegistry(vaultBase) {
|
|
|
276
276
|
|
|
277
277
|
try {
|
|
278
278
|
const parsed = JSON.parse(readFileSync(checked.target, 'utf-8'));
|
|
279
|
+
const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
279
280
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
281
|
+
...root,
|
|
282
|
+
version: Math.max(2, root.version || 1),
|
|
283
|
+
sessions: root.sessions && typeof root.sessions === 'object' && !Array.isArray(root.sessions)
|
|
284
|
+
? root.sessions : {},
|
|
282
285
|
};
|
|
283
286
|
} catch {
|
|
284
287
|
return { version: 2, sessions: {} };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.76.
|
|
3
|
+
"version": "0.76.4",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -59,8 +59,12 @@ Usage:
|
|
|
59
59
|
Managed linked worktrees under .worktrees (branch default wk/<slug>).
|
|
60
60
|
wendkeep context switch <branch> [--create] [--session <id>] [--json]
|
|
61
61
|
Switch Git branch and the causal session scope in the same worktree.
|
|
62
|
+
wendkeep context status --session <id> [--json]
|
|
63
|
+
wendkeep context recover --session <id> --select reserved|observed --revision <n> --reason <text> [--json]
|
|
64
|
+
Inspect or explicitly recover a quarantined causal scope conflict.
|
|
62
65
|
wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
|
|
63
66
|
status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
|
|
67
|
+
--session <id> selects the causal active_context for implicit change operations.
|
|
64
68
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
65
69
|
backlink [--apply]: injeta o backlink pro proposta em design/tarefas/spec órfãos (open + _arquivo).
|
|
66
70
|
wendkeep theme sync Re-aplica o color system (snippet CSS + graph color groups) num vault
|
|
@@ -73,7 +77,7 @@ Usage:
|
|
|
73
77
|
FLOW records scope, sensors and a receipt without creating a change.
|
|
74
78
|
wendkeep delivery <sub> Operational delivery: start | status | finish | abandon.
|
|
75
79
|
Records authorization and an append-only receipt; never creates a change/spec/ADR.
|
|
76
|
-
wendkeep spec <sub> Specs: list | show | effective [--change] [--json] | migrate | rebase.
|
|
80
|
+
wendkeep spec <sub> Specs: list | show | effective [--change] [--session] [--json] | migrate | rebase.
|
|
77
81
|
wendkeep sensors <sub> list | add <id> "<command>" [--severity --type --report].
|
|
78
82
|
wendkeep cost [opts] Aggregate AI-coding spend across the vault's sessions.
|
|
79
83
|
--since <date> · --top [N] (priciest) · --trend [day|week|month]
|
|
@@ -88,7 +92,7 @@ Usage:
|
|
|
88
92
|
--rescan-decisions (capture prose decisions from already-imported transcripts) ·
|
|
89
93
|
--from <dir> · --codex-from <dir> · --since <date> · --limit N ·
|
|
90
94
|
--dry-run · --json.
|
|
91
|
-
wendkeep verify [--deep] [--change s] Run a change's task sensors + record evidence (the gate);
|
|
95
|
+
wendkeep verify [--deep] [--change s] [--session id] Run a change's task sensors + record evidence (the gate);
|
|
92
96
|
--deep assembles the verification package for the wk-verify pass.
|
|
93
97
|
wendkeep dashboard [--force] (Re)generate the vault's folder-filtered Bases + 00-Dashboard MOC.
|
|
94
98
|
wendkeep renumber-decisions Renumber 04-Decisões to ADR-<NNNN>-<slug> in chronological order,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
2
|
+
import { migrateLegacyActiveContext } from '../hooks/active-context-store.mjs';
|
|
3
|
+
import { captureProjectScope, compareProjectScopes } from '../hooks/project-scope.mjs';
|
|
4
|
+
import {
|
|
5
|
+
discoverWorktreeRepository,
|
|
6
|
+
readWorktreeRegistry,
|
|
7
|
+
worktreeIdentity,
|
|
8
|
+
} from '../packages/vault/src/worktree-metadata.mjs';
|
|
9
|
+
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
10
|
+
|
|
11
|
+
function runtimeError(code, message) {
|
|
12
|
+
const error = new Error(message);
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function activeSession(entry) {
|
|
18
|
+
return entry?.status === 'active'
|
|
19
|
+
&& entry?.project_scope?.complete === true
|
|
20
|
+
&& typeof entry?.work_session_id === 'string'
|
|
21
|
+
&& entry.work_session_id.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveRuntimeActiveContext({
|
|
25
|
+
vaultBase,
|
|
26
|
+
projectRoot = process.cwd(),
|
|
27
|
+
sessionId = '',
|
|
28
|
+
spawn,
|
|
29
|
+
} = {}) {
|
|
30
|
+
const project = readProjectForValidation(vaultBase);
|
|
31
|
+
if (!project.ok || !project.projectId) {
|
|
32
|
+
throw runtimeError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE', 'PROJECT.json não prova project_id');
|
|
33
|
+
}
|
|
34
|
+
let repository;
|
|
35
|
+
try { repository = discoverWorktreeRepository({ startDir: projectRoot, ...(spawn ? { spawn } : {}) }); }
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw runtimeError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE', error.message);
|
|
38
|
+
}
|
|
39
|
+
const metadata = readWorktreeRegistry(repository).registry;
|
|
40
|
+
if (!metadata) {
|
|
41
|
+
throw runtimeError(
|
|
42
|
+
'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE',
|
|
43
|
+
'registry de worktrees ausente; não é seguro inventar repository_id/worktree_id',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (metadata.projectId !== project.projectId) {
|
|
47
|
+
throw runtimeError('WENDKEEP_ACTIVE_CONTEXT_IDENTITY_MISMATCH', 'registry de worktrees pertence a outro projeto');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const registry = readSessionRegistry(vaultBase);
|
|
51
|
+
const sessions = registry.sessions || {};
|
|
52
|
+
const worktreeId = worktreeIdentity(metadata.repositoryId, repository.gitDir);
|
|
53
|
+
const requested = String(sessionId || '').trim();
|
|
54
|
+
const rows = requested ? [[requested, sessions[requested]]] : Object.entries(sessions);
|
|
55
|
+
const matches = [];
|
|
56
|
+
for (const [candidateId, entry] of rows) {
|
|
57
|
+
if (!activeSession(entry)) continue;
|
|
58
|
+
const actual = captureProjectScope({
|
|
59
|
+
input: { cwd: projectRoot },
|
|
60
|
+
projectRoot,
|
|
61
|
+
projectId: project.projectId,
|
|
62
|
+
provider: entry.provider || '',
|
|
63
|
+
sessionId: candidateId,
|
|
64
|
+
...(spawn ? { spawn } : {}),
|
|
65
|
+
});
|
|
66
|
+
if (!compareProjectScopes(entry.project_scope, actual).ok) continue;
|
|
67
|
+
matches.push({ sessionId: candidateId, entry, actual });
|
|
68
|
+
}
|
|
69
|
+
if (!matches.length) {
|
|
70
|
+
throw runtimeError(
|
|
71
|
+
'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND',
|
|
72
|
+
requested ? 'sessão causal não corresponde à worktree atual' : 'nenhuma sessão ativa corresponde à worktree atual',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
let selected = matches[0];
|
|
76
|
+
if (matches.length > 1) {
|
|
77
|
+
const active = Object.values(registry.active_contexts || {}).filter((context) => (
|
|
78
|
+
context?.state === 'active'
|
|
79
|
+
&& context.project_id === project.projectId
|
|
80
|
+
&& context.repository_id === metadata.repositoryId
|
|
81
|
+
&& context.worktree_id === worktreeId
|
|
82
|
+
));
|
|
83
|
+
const narrowed = active.length === 1
|
|
84
|
+
? matches.filter(({ entry }) => String(entry.work_session_id) === active[0].work_session_id)
|
|
85
|
+
: [];
|
|
86
|
+
if (narrowed.length !== 1) {
|
|
87
|
+
throw runtimeError(
|
|
88
|
+
'WENDKEEP_ACTIVE_CONTEXT_AMBIGUOUS',
|
|
89
|
+
'mais de uma sessão ativa corresponde à worktree; informe --session',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
[selected] = narrowed;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
projectId: project.projectId,
|
|
96
|
+
repositoryId: metadata.repositoryId,
|
|
97
|
+
worktreeId,
|
|
98
|
+
workSessionId: String(selected.entry.work_session_id),
|
|
99
|
+
branch: selected.actual.branch,
|
|
100
|
+
headSha: selected.actual.head,
|
|
101
|
+
sessionId: selected.sessionId,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveCommandActiveContext({
|
|
106
|
+
vaultBase,
|
|
107
|
+
projectRoot = process.cwd(),
|
|
108
|
+
sessionId = '',
|
|
109
|
+
spawn,
|
|
110
|
+
} = {}) {
|
|
111
|
+
const requestedSession = String(sessionId || '').trim();
|
|
112
|
+
let identity;
|
|
113
|
+
try {
|
|
114
|
+
identity = resolveRuntimeActiveContext({
|
|
115
|
+
vaultBase, projectRoot, sessionId: requestedSession, ...(spawn ? { spawn } : {}),
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const registry = readSessionRegistry(vaultBase);
|
|
119
|
+
const initialized = Object.keys(registry.active_contexts || {}).length > 0;
|
|
120
|
+
const legacyUnavailable = error?.code === 'WENDKEEP_ACTIVE_CONTEXT_IDENTITY_UNAVAILABLE'
|
|
121
|
+
|| (!requestedSession && error?.code === 'WENDKEEP_ACTIVE_CONTEXT_NOT_FOUND');
|
|
122
|
+
if (!initialized && legacyUnavailable) return null;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
migrateLegacyActiveContext(vaultBase, {
|
|
126
|
+
identityForSession: (candidateId) => resolveRuntimeActiveContext({
|
|
127
|
+
vaultBase, projectRoot, sessionId: candidateId, ...(spawn ? { spawn } : {}),
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
return identity;
|
|
131
|
+
}
|
package/src/change.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
backfillArtifactLinks,
|
|
18
18
|
scaffoldPlaceholders,
|
|
19
19
|
isGuideCompactChange,
|
|
20
|
+
setActiveChange,
|
|
20
21
|
} from '../hooks/change-core.mjs';
|
|
21
22
|
import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
|
|
22
23
|
import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
|
|
@@ -24,6 +25,7 @@ import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegist
|
|
|
24
25
|
import { getLocale } from '../hooks/locale.mjs';
|
|
25
26
|
import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
|
|
26
27
|
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
28
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
27
29
|
|
|
28
30
|
function observerMarkdownUnder(vaultBase, relativeRoot) {
|
|
29
31
|
const output = [];
|
|
@@ -71,6 +73,24 @@ export function runChange(argv) {
|
|
|
71
73
|
const vaultBase = resolveVault(rest);
|
|
72
74
|
const VALUE_FLAGS = new Set(['--vault', '--change', '--project', '--session']);
|
|
73
75
|
const slugArg = () => rest.find((a, i) => !a.startsWith('-') && !VALUE_FLAGS.has(rest[i - 1]));
|
|
76
|
+
const projectRoot = resolve(opt(rest, '--project') || process.cwd());
|
|
77
|
+
let contextResolved = false;
|
|
78
|
+
let resolvedContext = null;
|
|
79
|
+
const context = () => {
|
|
80
|
+
if (contextResolved) return resolvedContext;
|
|
81
|
+
contextResolved = true;
|
|
82
|
+
const sessionId = opt(rest, '--session')
|
|
83
|
+
|| process.env.CODEX_THREAD_ID
|
|
84
|
+
|| process.env.CLAUDE_SESSION_ID
|
|
85
|
+
|| '';
|
|
86
|
+
try {
|
|
87
|
+
resolvedContext = resolveCommandActiveContext({ vaultBase, projectRoot, sessionId });
|
|
88
|
+
return resolvedContext;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
process.stderr.write(`wendkeep change: ${error.code || 'WENDKEEP_ACTIVE_CONTEXT_FAILED'}: ${error.message}\n`);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
74
94
|
|
|
75
95
|
if (sub === 'new') {
|
|
76
96
|
const slug = slugArg();
|
|
@@ -80,6 +100,7 @@ export function runChange(argv) {
|
|
|
80
100
|
try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* sem control */ }
|
|
81
101
|
const r = newChange(vaultBase, slug, {
|
|
82
102
|
dateStr: today(), simple: rest.includes('--simple'), guide: rest.includes('--guide'), sessionRel,
|
|
103
|
+
context: context(),
|
|
83
104
|
});
|
|
84
105
|
process.stdout.write(`change ${r.created ? 'created' : 'exists'}: ${r.rel} (active)\n`);
|
|
85
106
|
process.exit(0);
|
|
@@ -88,7 +109,7 @@ export function runChange(argv) {
|
|
|
88
109
|
if (sub === 'use') {
|
|
89
110
|
const slug = slugArg();
|
|
90
111
|
if (!slug) { process.stderr.write('wendkeep change use: missing <slug>\n'); process.exit(2); }
|
|
91
|
-
const r = useChange(vaultBase, slug);
|
|
112
|
+
const r = useChange(vaultBase, slug, { context: context() });
|
|
92
113
|
if (!r.ok) { process.stderr.write(`wendkeep change use: ${r.error}\n`); process.exit(2); }
|
|
93
114
|
process.stdout.write(`current change: ${slug}\n`);
|
|
94
115
|
process.exit(0);
|
|
@@ -102,6 +123,8 @@ export function runChange(argv) {
|
|
|
102
123
|
if (!state.changes.some((item) => item.slug === slug)) { process.stderr.write(`wendkeep change bind: open change not found: ${slug}\n`); process.exit(2); }
|
|
103
124
|
if (!readSessionRegistry(vaultBase).sessions?.[sessionId]) { process.stderr.write(`wendkeep change bind: session not found: ${sessionId}\n`); process.exit(2); }
|
|
104
125
|
upsertSessionRegistry(vaultBase, sessionId, { change_slug: slug });
|
|
126
|
+
const selectedContext = context();
|
|
127
|
+
if (selectedContext) setActiveChange(vaultBase, slug, { context: selectedContext });
|
|
105
128
|
process.stdout.write(`session ${sessionId} -> change ${slug}\n`);
|
|
106
129
|
process.exit(0);
|
|
107
130
|
}
|
|
@@ -117,6 +140,7 @@ export function runChange(argv) {
|
|
|
117
140
|
try { sessionRel = readControl(vaultBase).session_file || ''; } catch { /* no control */ }
|
|
118
141
|
const r = continueChange(vaultBase, archivedSlug, newSlug, {
|
|
119
142
|
dateStr: today(), simple: rest.includes('--simple'), guide: rest.includes('--guide'), sessionRel,
|
|
143
|
+
context: context(),
|
|
120
144
|
});
|
|
121
145
|
if (!r.ok) { process.stderr.write(`wendkeep change continue: ${r.error}\n`); process.exit(2); }
|
|
122
146
|
process.stdout.write(`change created: ${r.rel} (continues ${r.archived}; active)\n`);
|
|
@@ -124,7 +148,7 @@ export function runChange(argv) {
|
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
if (sub === 'list') {
|
|
127
|
-
const state = allChangesState(vaultBase);
|
|
151
|
+
const state = allChangesState(vaultBase, { context: context() });
|
|
128
152
|
const { archived } = listChanges(vaultBase);
|
|
129
153
|
process.stdout.write(`${renderOpenChanges(state, { tag: '' }) || 'open changes: (none)'}\n`);
|
|
130
154
|
process.stdout.write(`archived: ${archived.join(', ') || '(none)'}\n`);
|
|
@@ -147,7 +171,7 @@ export function runChange(argv) {
|
|
|
147
171
|
if (sub === 'status') {
|
|
148
172
|
const slug = slugArg();
|
|
149
173
|
if (!slug) {
|
|
150
|
-
const state = allChangesState(vaultBase);
|
|
174
|
+
const state = allChangesState(vaultBase, { context: context() });
|
|
151
175
|
if (!state.changes.length && !state.pointerWarning) {
|
|
152
176
|
process.stderr.write('wendkeep change status: no open changes\n');
|
|
153
177
|
process.exit(2);
|
|
@@ -161,7 +185,7 @@ export function runChange(argv) {
|
|
|
161
185
|
catch { process.stderr.write(`wendkeep change status: not found: ${slug}\n`); process.exit(2); }
|
|
162
186
|
const tasks = parseTasks(tarefasMd);
|
|
163
187
|
const done = tasks.filter((t) => t.done).length;
|
|
164
|
-
process.stdout.write(`change: ${slug}${slug === activeChange(vaultBase) ? ' (ativa)' : ''}\n`);
|
|
188
|
+
process.stdout.write(`change: ${slug}${slug === activeChange(vaultBase, { context: context() }) ? ' (ativa)' : ''}\n`);
|
|
165
189
|
let specs = [];
|
|
166
190
|
try { specs = parseSpecsList(readFileSync(join(dir, 'proposta.md'), 'utf8')); } catch { /* sem proposta */ }
|
|
167
191
|
process.stdout.write(`specs: ${specs.join(', ') || '(nenhuma)'}\n`);
|
|
@@ -194,7 +218,7 @@ export function runChange(argv) {
|
|
|
194
218
|
if (sub === 'done' || sub === 'undone') {
|
|
195
219
|
const taskId = slugArg();
|
|
196
220
|
if (!taskId) { process.stderr.write(`wendkeep change ${sub}: missing <taskId>\n`); process.exit(2); }
|
|
197
|
-
const slug = opt(rest, '--change') || activeChange(vaultBase);
|
|
221
|
+
const slug = opt(rest, '--change') || activeChange(vaultBase, { context: context() });
|
|
198
222
|
if (!slug) { process.stderr.write(`wendkeep change ${sub}: no active change\n`); process.exit(2); }
|
|
199
223
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
200
224
|
let ok = false;
|
|
@@ -205,7 +229,7 @@ export function runChange(argv) {
|
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
if (sub === 'diff') {
|
|
208
|
-
const slug = slugArg() || activeChange(vaultBase);
|
|
232
|
+
const slug = slugArg() || activeChange(vaultBase, { context: context() });
|
|
209
233
|
if (!slug) { process.stderr.write('wendkeep change diff: no change (arg or active)\n'); process.exit(2); }
|
|
210
234
|
const dir = join(vaultBase, getLocale(vaultBase).folders.changes, slug);
|
|
211
235
|
let specs = [];
|
|
@@ -228,7 +252,8 @@ export function runChange(argv) {
|
|
|
228
252
|
}
|
|
229
253
|
|
|
230
254
|
if (sub === 'archive') {
|
|
231
|
-
const
|
|
255
|
+
const selectedContext = context();
|
|
256
|
+
const slug = slugArg() || activeChange(vaultBase, { context: selectedContext });
|
|
232
257
|
if (!slug) { process.stderr.write('wendkeep change archive: missing <slug> and no active change\n'); process.exit(2); }
|
|
233
258
|
// Real gate (Pilar C): every sensor a task declared must be green in evidencia.json.
|
|
234
259
|
const gate = (dir) => {
|
|
@@ -310,7 +335,10 @@ export function runChange(argv) {
|
|
|
310
335
|
if (trivial) process.stderr.write(compactGuide
|
|
311
336
|
? 'aviso: GUIDE compacta sem [req:]/[sensor:] — resultado permanece auditável no archive, sem ADR automático\n'
|
|
312
337
|
: 'aviso: change trivial (sem [req:]/[sensor:]) — ADR marcado trivial: true\n');
|
|
313
|
-
const r = archiveChange(vaultBase, slug, {
|
|
338
|
+
const r = archiveChange(vaultBase, slug, {
|
|
339
|
+
dateStr: today(), adrNum: getNextAdrNumber(vaultBase), gate,
|
|
340
|
+
adrFlags: { forced, trivial }, context: selectedContext,
|
|
341
|
+
});
|
|
314
342
|
if (!r.ok) {
|
|
315
343
|
process.stderr.write(`change archive BLOCKED (gate): ${r.failing.join('; ')}\n`);
|
|
316
344
|
process.exit(1);
|
|
@@ -366,9 +394,10 @@ export function runChange(argv) {
|
|
|
366
394
|
}
|
|
367
395
|
|
|
368
396
|
if (sub === 'abandon') {
|
|
369
|
-
const
|
|
397
|
+
const selectedContext = context();
|
|
398
|
+
const slug = slugArg() || activeChange(vaultBase, { context: selectedContext });
|
|
370
399
|
if (!slug) { process.stderr.write('wendkeep change abandon: missing <slug> and no active change\n'); process.exit(2); }
|
|
371
|
-
const r = abandonChange(vaultBase, slug, { dateStr: today() });
|
|
400
|
+
const r = abandonChange(vaultBase, slug, { dateStr: today(), context: selectedContext });
|
|
372
401
|
if (!r.ok) { process.stderr.write(`wendkeep change abandon: ${r.failing.join('; ')}\n`); process.exit(2); }
|
|
373
402
|
process.stdout.write(`abandoned: ${r.archivedRel}\n`);
|
|
374
403
|
process.exit(0);
|