wendkeep 0.72.1 → 0.73.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 +30 -0
- package/README.en.md +28 -11
- package/README.md +28 -11
- package/docs/en/commands/changes-and-verification.md +10 -5
- package/docs/en/commands/maintenance-and-diagnostics.md +17 -9
- package/docs/en/commands/operating-profiles.md +28 -3
- package/docs/pt-BR/commands/changes-and-verification.md +10 -5
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -5
- package/docs/pt-BR/commands/operating-profiles.md +28 -3
- package/hooks/brain-inject.mjs +6 -6
- package/hooks/change-context.mjs +11 -0
- package/hooks/change-core.mjs +53 -21
- package/hooks/change-warn.mjs +2 -0
- package/hooks/harness-doctor.mjs +13 -5
- package/hooks/vault-health.mjs +2 -2
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +12 -2
- package/src/change.mjs +10 -4
- package/src/delivery.mjs +303 -0
- package/src/doctor.mjs +47 -10
- package/src/release-provenance.mjs +47 -0
- package/src/skills-seed.mjs +25 -9
- package/src/sync-defs.mjs +5 -2
- package/src/sync.mjs +2 -2
- package/src/work-kind.mjs +62 -0
package/src/delivery.mjs
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
assertVaultPathSafe, mkdirVaultPath, writeVaultFileSync,
|
|
6
|
+
} from '../hooks/vault-path-safety.mjs';
|
|
7
|
+
import { resolveProjectVault } from './project-vault.mjs';
|
|
8
|
+
import { createWorkRoute } from './work-kind.mjs';
|
|
9
|
+
|
|
10
|
+
export const DELIVERY_HELP = `wendkeep delivery <subcommand>
|
|
11
|
+
|
|
12
|
+
start [id] --allow <capability> [--source-change <slug>] [--source-commit <sha>]
|
|
13
|
+
status [id]
|
|
14
|
+
finish [id] [--target <ref>] [--ci-url <url>] [--version <x.y.z>]
|
|
15
|
+
[--npm-integrity <sha512-...>] [--release-url <url>]
|
|
16
|
+
abandon [id] --reason <text>
|
|
17
|
+
|
|
18
|
+
Common options: --project <path> --vault <path> --json
|
|
19
|
+
Delivery authorizes operational risk and creates an append-only receipt. It never creates a change,
|
|
20
|
+
spec, or ADR. If code/config must change, abandon or pause delivery and resume an implementation.
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
export const DELIVERY_CAPABILITIES = Object.freeze([
|
|
24
|
+
'git:merge', 'git:pull', 'git:push', 'git:tag', 'publish',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const VALUE_OPTIONS = new Set([
|
|
28
|
+
'--project', '--vault', '--allow', '--source-change', '--source-commit', '--target',
|
|
29
|
+
'--ci-url', '--version', '--npm-integrity', '--release-url', '--reason',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function parseArgv(argv) {
|
|
33
|
+
const values = new Map();
|
|
34
|
+
const positionals = [];
|
|
35
|
+
let json = false;
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const item = argv[index];
|
|
38
|
+
if (item === '--json') { json = true; continue; }
|
|
39
|
+
if (item.startsWith('--')) {
|
|
40
|
+
const eq = item.indexOf('=');
|
|
41
|
+
const name = eq > 0 ? item.slice(0, eq) : item;
|
|
42
|
+
if (!VALUE_OPTIONS.has(name)) throw new Error(`opção desconhecida: ${name}`);
|
|
43
|
+
const value = eq > 0 ? item.slice(eq + 1) : argv[++index];
|
|
44
|
+
if (!value || value.startsWith('--')) throw new Error(`${name} requer um valor`);
|
|
45
|
+
const list = values.get(name) || [];
|
|
46
|
+
list.push(value);
|
|
47
|
+
values.set(name, list);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
positionals.push(item);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
json,
|
|
54
|
+
positionals,
|
|
55
|
+
value: (name) => values.get(name)?.at(-1) || '',
|
|
56
|
+
all: (name) => values.get(name) || [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function git(projectRoot, args, optional = false) {
|
|
61
|
+
try {
|
|
62
|
+
return execFileSync('git', args, {
|
|
63
|
+
cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
64
|
+
}).trim();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (optional) return '';
|
|
67
|
+
throw new Error(`git ${args.join(' ')} falhou: ${String(error.stderr || error.message).trim()}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deliveryPaths(vaultBase, id = '') {
|
|
72
|
+
const runtime = join(vaultBase, '.brain', 'runtime');
|
|
73
|
+
const deliveries = join(runtime, 'deliveries');
|
|
74
|
+
return {
|
|
75
|
+
runtime,
|
|
76
|
+
deliveries,
|
|
77
|
+
pointer: join(runtime, 'CURRENT_DELIVERY'),
|
|
78
|
+
receipts: join(runtime, 'delivery-receipts.jsonl'),
|
|
79
|
+
state: id ? join(deliveries, `${id}.json`) : '',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function safeId(value) {
|
|
84
|
+
const id = String(value || '').trim();
|
|
85
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,100}$/i.test(id)) throw new Error('id de delivery inválido');
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function generatedId(now = new Date()) {
|
|
90
|
+
return `delivery-${now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z').toLowerCase()}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readPointer(vaultBase) {
|
|
94
|
+
const { pointer } = deliveryPaths(vaultBase);
|
|
95
|
+
try { return safeId(readFileSync(pointer, 'utf8').trim()); } catch { return ''; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function activeDelivery(vaultBase) {
|
|
99
|
+
const id = readPointer(vaultBase);
|
|
100
|
+
if (!id) return null;
|
|
101
|
+
try {
|
|
102
|
+
const state = readState(vaultBase, id);
|
|
103
|
+
return state.state === 'active' ? state : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readState(vaultBase, id) {
|
|
110
|
+
const path = deliveryPaths(vaultBase, id).state;
|
|
111
|
+
const checked = assertVaultPathSafe(vaultBase, path, {
|
|
112
|
+
allowMissing: false, expectedType: 'file', label: `delivery ${id}`,
|
|
113
|
+
});
|
|
114
|
+
return JSON.parse(readFileSync(checked.target, 'utf8'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeState(vaultBase, state) {
|
|
118
|
+
const paths = deliveryPaths(vaultBase, state.id);
|
|
119
|
+
mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
|
|
120
|
+
mkdirVaultPath(vaultBase, paths.deliveries, { label: 'estados de delivery' });
|
|
121
|
+
writeVaultFileSync(vaultBase, paths.state, `${JSON.stringify(state, null, 2)}\n`, 'utf8', {
|
|
122
|
+
label: `delivery ${state.id}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setPointer(vaultBase, id = '') {
|
|
127
|
+
const paths = deliveryPaths(vaultBase);
|
|
128
|
+
mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
|
|
129
|
+
writeVaultFileSync(vaultBase, paths.pointer, id ? `${id}\n` : '', 'utf8', {
|
|
130
|
+
label: 'ponteiro de delivery',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function appendReceipt(vaultBase, receipt) {
|
|
135
|
+
const paths = deliveryPaths(vaultBase);
|
|
136
|
+
mkdirVaultPath(vaultBase, paths.runtime, { label: 'runtime de delivery' });
|
|
137
|
+
let previous = '';
|
|
138
|
+
if (existsSync(paths.receipts)) {
|
|
139
|
+
const checked = assertVaultPathSafe(vaultBase, paths.receipts, {
|
|
140
|
+
allowMissing: false, expectedType: 'file', label: 'ledger de receipts de delivery',
|
|
141
|
+
});
|
|
142
|
+
previous = readFileSync(checked.target, 'utf8');
|
|
143
|
+
}
|
|
144
|
+
writeVaultFileSync(vaultBase, paths.receipts, `${previous}${JSON.stringify(receipt)}\n`, 'utf8', {
|
|
145
|
+
label: 'ledger de receipts de delivery',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function context(parsed) {
|
|
150
|
+
const projectRoot = resolve(parsed.value('--project') || process.cwd());
|
|
151
|
+
const resolved = resolveProjectVault({
|
|
152
|
+
startDir: projectRoot,
|
|
153
|
+
explicitVault: parsed.value('--vault'),
|
|
154
|
+
});
|
|
155
|
+
const repoRoot = git(projectRoot, ['rev-parse', '--show-toplevel']);
|
|
156
|
+
return { projectRoot, repoRoot, vaultBase: resolved.base };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function currentId(parsed, vaultBase) {
|
|
160
|
+
return safeId(parsed.positionals[1] || readPointer(vaultBase));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ensureClean(repoRoot) {
|
|
164
|
+
const dirty = git(repoRoot, ['status', '--porcelain']);
|
|
165
|
+
if (dirty) {
|
|
166
|
+
const error = new Error('delivery requer working tree limpa; alterações de código/config exigem implementation.');
|
|
167
|
+
error.code = 'WENDKEEP_DELIVERY_IMPLEMENTATION_REQUIRED';
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function startDelivery({ vaultBase, repoRoot, id, capabilities, sourceChange = '', sourceCommit = '', now = new Date() }) {
|
|
173
|
+
ensureClean(repoRoot);
|
|
174
|
+
const deliveryId = safeId(id || generatedId(now));
|
|
175
|
+
const paths = deliveryPaths(vaultBase, deliveryId);
|
|
176
|
+
if (existsSync(paths.state)) throw new Error(`delivery já existe: ${deliveryId}`);
|
|
177
|
+
const commit = sourceCommit || git(repoRoot, ['rev-parse', 'HEAD']);
|
|
178
|
+
git(repoRoot, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
179
|
+
const requestedCapabilities = [...new Set((capabilities || []).map((item) => String(item).trim()).filter(Boolean))];
|
|
180
|
+
const invalidCapabilities = requestedCapabilities.filter((item) => !DELIVERY_CAPABILITIES.includes(item));
|
|
181
|
+
if (invalidCapabilities.length) {
|
|
182
|
+
throw new Error(`capability inválida: ${invalidCapabilities.join(', ')}. Use ${DELIVERY_CAPABILITIES.join(', ')}.`);
|
|
183
|
+
}
|
|
184
|
+
const route = createWorkRoute({
|
|
185
|
+
workKind: 'delivery', profile: 'ASSURE', contractImpact: 'none',
|
|
186
|
+
operationRisk: requestedCapabilities, sourceChange, sourceCommit: commit,
|
|
187
|
+
});
|
|
188
|
+
if (!route.operation_risk.length) throw new Error('delivery start requer ao menos um --allow <capability>');
|
|
189
|
+
const state = {
|
|
190
|
+
schema_version: 1,
|
|
191
|
+
id: deliveryId,
|
|
192
|
+
state: 'active',
|
|
193
|
+
route,
|
|
194
|
+
repository: repoRoot,
|
|
195
|
+
worktree: repoRoot,
|
|
196
|
+
branch: git(repoRoot, ['branch', '--show-current'], true),
|
|
197
|
+
source_commit: commit,
|
|
198
|
+
started_at: now.toISOString(),
|
|
199
|
+
};
|
|
200
|
+
writeState(vaultBase, state);
|
|
201
|
+
setPointer(vaultBase, deliveryId);
|
|
202
|
+
return state;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function finishDelivery({ vaultBase, repoRoot, id, target = 'HEAD', evidence = {}, now = new Date() }) {
|
|
206
|
+
ensureClean(repoRoot);
|
|
207
|
+
const state = readState(vaultBase, safeId(id));
|
|
208
|
+
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
209
|
+
const targetCommit = git(repoRoot, ['rev-parse', `${target}^{commit}`]);
|
|
210
|
+
git(repoRoot, ['merge-base', '--is-ancestor', state.source_commit, targetCommit]);
|
|
211
|
+
const capabilities = state.route?.operation_risk || [];
|
|
212
|
+
if (capabilities.includes('git:tag') || capabilities.includes('publish')) {
|
|
213
|
+
if (!evidence.version) throw new Error('delivery com tag/publicação requer --version');
|
|
214
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
|
|
215
|
+
if (pkg.version !== evidence.version) throw new Error(`package.json ${pkg.version} diverge de ${evidence.version}`);
|
|
216
|
+
const tagCommit = git(repoRoot, ['rev-list', '-n', '1', `refs/tags/v${evidence.version}`]);
|
|
217
|
+
if (tagCommit !== targetCommit) throw new Error(`v${evidence.version} não aponta para o target comprovado`);
|
|
218
|
+
}
|
|
219
|
+
if (capabilities.includes('publish')) {
|
|
220
|
+
for (const [key, label] of [
|
|
221
|
+
['ci_url', '--ci-url'], ['npm_integrity', '--npm-integrity'], ['release_url', '--release-url'],
|
|
222
|
+
]) if (!evidence[key]) throw new Error(`delivery com publish requer ${label}`);
|
|
223
|
+
}
|
|
224
|
+
const receipt = {
|
|
225
|
+
schema_version: 1,
|
|
226
|
+
delivery_id: state.id,
|
|
227
|
+
outcome: 'completed',
|
|
228
|
+
work_kind: 'delivery',
|
|
229
|
+
source_change: state.route.source_change || '',
|
|
230
|
+
source_commit: state.source_commit,
|
|
231
|
+
target,
|
|
232
|
+
target_commit: targetCommit,
|
|
233
|
+
capabilities,
|
|
234
|
+
evidence,
|
|
235
|
+
finished_at: now.toISOString(),
|
|
236
|
+
};
|
|
237
|
+
appendReceipt(vaultBase, receipt);
|
|
238
|
+
writeState(vaultBase, { ...state, state: 'completed', target, target_commit: targetCommit, finished_at: receipt.finished_at, receipt });
|
|
239
|
+
if (readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
240
|
+
return receipt;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function abandonDelivery({ vaultBase, id, reason, now = new Date() }) {
|
|
244
|
+
const state = readState(vaultBase, safeId(id));
|
|
245
|
+
if (state.state !== 'active') throw new Error(`delivery ${id} não está ativa`);
|
|
246
|
+
if (!String(reason || '').trim()) throw new Error('delivery abandon requer --reason <text>');
|
|
247
|
+
const receipt = {
|
|
248
|
+
schema_version: 1, delivery_id: state.id, outcome: 'abandoned',
|
|
249
|
+
reason: String(reason).trim(), abandoned_at: now.toISOString(),
|
|
250
|
+
};
|
|
251
|
+
appendReceipt(vaultBase, receipt);
|
|
252
|
+
writeState(vaultBase, { ...state, state: 'abandoned', reason: receipt.reason, abandoned_at: receipt.abandoned_at });
|
|
253
|
+
if (readPointer(vaultBase) === state.id) setPointer(vaultBase);
|
|
254
|
+
return receipt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function emit(payload, json) {
|
|
258
|
+
if (json) process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
259
|
+
else process.stdout.write(`${payload.id || payload.delivery_id}: ${payload.state || payload.outcome}\n`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function runDelivery(argv = []) {
|
|
263
|
+
try {
|
|
264
|
+
const parsed = parseArgv(argv);
|
|
265
|
+
const sub = parsed.positionals[0] || 'status';
|
|
266
|
+
const { repoRoot, vaultBase } = context(parsed);
|
|
267
|
+
if (sub === 'start') {
|
|
268
|
+
const state = startDelivery({
|
|
269
|
+
vaultBase, repoRoot, id: parsed.positionals[1], capabilities: parsed.all('--allow'),
|
|
270
|
+
sourceChange: parsed.value('--source-change'), sourceCommit: parsed.value('--source-commit'),
|
|
271
|
+
});
|
|
272
|
+
emit(state, parsed.json);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
if (sub === 'status') {
|
|
276
|
+
const state = readState(vaultBase, currentId(parsed, vaultBase));
|
|
277
|
+
emit(state, parsed.json);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
if (sub === 'finish') {
|
|
281
|
+
const receipt = finishDelivery({
|
|
282
|
+
vaultBase, repoRoot, id: currentId(parsed, vaultBase), target: parsed.value('--target') || 'HEAD',
|
|
283
|
+
evidence: {
|
|
284
|
+
ci_url: parsed.value('--ci-url'), version: parsed.value('--version'),
|
|
285
|
+
npm_integrity: parsed.value('--npm-integrity'), release_url: parsed.value('--release-url'),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
emit(receipt, parsed.json);
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
if (sub === 'abandon') {
|
|
292
|
+
const receipt = abandonDelivery({
|
|
293
|
+
vaultBase, id: currentId(parsed, vaultBase), reason: parsed.value('--reason'),
|
|
294
|
+
});
|
|
295
|
+
emit(receipt, parsed.json);
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`subcomando desconhecido: ${sub}`);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
process.stderr.write(`wendkeep delivery: ${error.message}\n`);
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { checkSyncDefs } from './sync-defs.mjs';
|
|
|
7
7
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
8
8
|
|
|
9
9
|
const healthStatusLabel = (status) => ({
|
|
10
|
-
healthy: 'saudável', warning: 'atenção', blocked: 'bloqueada', legacy: 'legado',
|
|
10
|
+
healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
|
|
11
11
|
}[status] || status || 'desconhecido');
|
|
12
12
|
|
|
13
13
|
const metricValue = (value) => value === null || value === undefined || value === '' ? 'n/a' : value;
|
|
@@ -55,6 +55,8 @@ export function runDoctor(argv) {
|
|
|
55
55
|
let vault;
|
|
56
56
|
let project;
|
|
57
57
|
let session = '';
|
|
58
|
+
let scope = 'all';
|
|
59
|
+
let strict = false;
|
|
58
60
|
for (let i = 0; i < argv.length; i += 1) {
|
|
59
61
|
const a = argv[i];
|
|
60
62
|
if (a === '--vault') vault = argv[++i];
|
|
@@ -63,6 +65,13 @@ export function runDoctor(argv) {
|
|
|
63
65
|
else if (a.startsWith('--project=')) project = a.slice(10);
|
|
64
66
|
else if (a === '--session') session = argv[++i] || '';
|
|
65
67
|
else if (a.startsWith('--session=')) session = a.slice(10);
|
|
68
|
+
else if (a === '--scope') scope = argv[++i] || '';
|
|
69
|
+
else if (a.startsWith('--scope=')) scope = a.slice(8);
|
|
70
|
+
else if (a === '--strict') strict = true;
|
|
71
|
+
}
|
|
72
|
+
if (!['all', 'core', 'runtime'].includes(scope)) {
|
|
73
|
+
process.stderr.write('wendkeep doctor: --scope deve ser all, core ou runtime\n');
|
|
74
|
+
return 2;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
const projectRoot = resolve(project || process.cwd());
|
|
@@ -97,18 +106,29 @@ export function runDoctor(argv) {
|
|
|
97
106
|
memoryStatus: 'blocked',
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
|
-
process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
|
|
109
|
+
if (scope !== 'runtime') process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
|
|
101
110
|
const healthStatus = health.ok ? 0 : 1;
|
|
102
111
|
|
|
112
|
+
if (scope === 'core') {
|
|
113
|
+
const strictDebt = strict && (
|
|
114
|
+
(health.warnings || []).length > 0
|
|
115
|
+
|| !['healthy'].includes(health.memoryStatus)
|
|
116
|
+
);
|
|
117
|
+
process.stdout.write(`\n[core] ${healthStatus ? 'erro estrutural' : health.memoryStatus === 'degraded' ? 'saudável com memória degradada' : 'saudável'}\n`);
|
|
118
|
+
return healthStatus || strictDebt ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
// 2. Harness integrity (Wave B).
|
|
104
|
-
const { errors, warnings } = checkHarness(vaultBase, projectRoot);
|
|
122
|
+
const { errors, warnings, attention, repairable } = checkHarness(vaultBase, projectRoot);
|
|
105
123
|
const defs = checkSyncDefs(vaultBase, projectRoot);
|
|
106
124
|
if (!defs.ok) {
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
repairable.push(...defs.issues.map((issue) => `defs: ${issue}`));
|
|
126
|
+
repairable.push('defs stale — rode `wendkeep sync-defs --reseed` e reinicie Claude Code/Codex');
|
|
109
127
|
}
|
|
110
|
-
process.stdout.write(`\n[
|
|
128
|
+
process.stdout.write(`\n[runtime] ${errors.length} erro(s) estrutural(is), ${attention.length} atenção(ões), ${repairable.length} reparável(is), ${warnings.length} aviso(s)\n`);
|
|
111
129
|
for (const e of errors) process.stdout.write(` ✗ ${e}\n`);
|
|
130
|
+
for (const item of attention) process.stdout.write(` ! ${item}\n`);
|
|
131
|
+
for (const item of repairable) process.stdout.write(` → ${item}\n`);
|
|
112
132
|
for (const w of warnings) process.stdout.write(` ! ${w}\n`);
|
|
113
133
|
|
|
114
134
|
// 3. Link/graph health — órfãos que o grafo do Obsidian mostraria, com o comando de reparo.
|
|
@@ -125,13 +145,16 @@ export function runDoctor(argv) {
|
|
|
125
145
|
process.stdout.write(`\n${renderStackedFrontmatterLines(vaultBase, stacked).join('\n')}\n`);
|
|
126
146
|
|
|
127
147
|
// 3c. Modelo fora de pricing.json fecha a sessão com custo zero, sem erro — só aparece aqui.
|
|
128
|
-
|
|
148
|
+
const unpriced = checkUnpricedModels(vaultBase);
|
|
149
|
+
process.stdout.write(`\n${renderUnpricedModelLines(unpriced).join('\n')}\n`);
|
|
129
150
|
|
|
130
151
|
// 3d. Seções derivadas do corpo que ficaram para trás do Encerramento (notas pré-0.53.0).
|
|
131
|
-
|
|
152
|
+
const staleDerived = checkStaleDerivedSections(vaultBase);
|
|
153
|
+
process.stdout.write(`\n${renderStaleDerivedSectionLines(staleDerived).join('\n')}\n`);
|
|
132
154
|
|
|
133
155
|
// 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
|
|
134
|
-
|
|
156
|
+
const observability = checkSessionObservability(vaultBase);
|
|
157
|
+
process.stdout.write(`\n${renderSessionObservabilityLines(observability).join('\n')}\n`);
|
|
135
158
|
|
|
136
159
|
// 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
|
|
137
160
|
const act = checkSessionActivity(vaultBase);
|
|
@@ -146,5 +169,19 @@ export function runDoctor(argv) {
|
|
|
146
169
|
|
|
147
170
|
// Devolve o código em vez de sair: `wendkeep sync` encadeia este comando, e um
|
|
148
171
|
// process.exit aqui mataria a cadeia. Quem faz o exit é o bin.
|
|
149
|
-
|
|
172
|
+
const strictDebt = strict && (
|
|
173
|
+
(scope !== 'runtime' && (health.warnings || []).length)
|
|
174
|
+
|| (scope !== 'runtime' && health.memoryStatus !== 'healthy')
|
|
175
|
+
|| attention.length
|
|
176
|
+
|| repairable.length
|
|
177
|
+
|| warnings.length
|
|
178
|
+
|| links.derivedOrphans
|
|
179
|
+
|| links.artifactOrphans
|
|
180
|
+
|| links.graphColors === false
|
|
181
|
+
|| stacked.count
|
|
182
|
+
|| (unpriced.models || unpriced.items || []).length
|
|
183
|
+
|| (staleDerived.notes || staleDerived.items || []).length
|
|
184
|
+
|| !observability.ok
|
|
185
|
+
);
|
|
186
|
+
return (scope !== 'runtime' && healthStatus !== 0) || errors.length || strictDebt ? 1 : 0;
|
|
150
187
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { cpSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
|
|
1
6
|
const DEPENDENCY_FIELDS = Object.freeze([
|
|
2
7
|
'dependencies',
|
|
3
8
|
'devDependencies',
|
|
@@ -5,6 +10,48 @@ const DEPENDENCY_FIELDS = Object.freeze([
|
|
|
5
10
|
'peerDependencies',
|
|
6
11
|
]);
|
|
7
12
|
|
|
13
|
+
export function parsePackIntegrity(raw) {
|
|
14
|
+
const text = String(raw || '');
|
|
15
|
+
const start = text.indexOf('[');
|
|
16
|
+
const end = text.lastIndexOf(']');
|
|
17
|
+
if (start < 0 || end < start) return '';
|
|
18
|
+
try {
|
|
19
|
+
return String(JSON.parse(text.slice(start, end + 1))[0]?.integrity || '');
|
|
20
|
+
} catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function packIntegrityInIsolatedCopy(root, { execute = execFileSync } = {}) {
|
|
26
|
+
const tempRoot = mkdtempSync(join(tmpdir(), 'wendkeep-release-pack-'));
|
|
27
|
+
const packageRoot = join(tempRoot, 'package');
|
|
28
|
+
const ignored = new Set(['.git', 'node_modules']);
|
|
29
|
+
try {
|
|
30
|
+
const binding = JSON.parse(readFileSync(join(root, '.wendkeep.json'), 'utf8'));
|
|
31
|
+
const vault = String(binding.vault || '');
|
|
32
|
+
if (vault && !vault.includes('/') && !vault.includes('\\')) ignored.add(vault);
|
|
33
|
+
} catch { /* unbound package: nothing else to exclude */ }
|
|
34
|
+
try {
|
|
35
|
+
cpSync(root, packageRoot, {
|
|
36
|
+
recursive: true,
|
|
37
|
+
filter(source) {
|
|
38
|
+
if (source === root) return true;
|
|
39
|
+
const relativeName = basename(source);
|
|
40
|
+
return !ignored.has(relativeName);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
44
|
+
const raw = execute(command, ['pack', '--dry-run', '--json'], {
|
|
45
|
+
cwd: packageRoot,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
});
|
|
49
|
+
return parsePackIntegrity(raw);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
8
55
|
export function packageHasSelfDependency(pkg = {}) {
|
|
9
56
|
const name = String(pkg.name || '');
|
|
10
57
|
if (!name) return false;
|
package/src/skills-seed.mjs
CHANGED
|
@@ -27,10 +27,15 @@ pode persistir \`profile use OFF\` explicitamente.
|
|
|
27
27
|
|
|
28
28
|
- **FLOW:** ajuste local, reversível e de escopo fechado, sem contrato/spec, segurança,
|
|
29
29
|
dependência, CI/release ou policy.
|
|
30
|
-
- **GUIDE:**
|
|
30
|
+
- **GUIDE:** implementação compacta com change, sem spec/design/ADR automáticos quando contract_impact é none.
|
|
31
31
|
- **GOVERN:** escolha conservadora em caso de dúvida ou risco e para superfícies sensíveis.
|
|
32
32
|
- **ASSURE:** GOVERN quando confirmação explícita e handoff fazem parte do contrato.
|
|
33
33
|
|
|
34
|
+
Classifique também o work kind: inspection, maintenance, implementation, delivery ou recovery.
|
|
35
|
+
Risco operacional e impacto de contrato são independentes. Merge, push, tag e publish de código
|
|
36
|
+
já aprovado usam delivery + ASSURE sem criar outra change; registre com delivery start e conclua
|
|
37
|
+
com delivery finish para gerar receipt.
|
|
38
|
+
|
|
34
39
|
O harness da LLM faz essa classificação semântica; o Wend Runtime valida e aplica a lease.
|
|
35
40
|
Se não houver uma sessão causal identificada ou o comando falhar, não fabrique estado: use o
|
|
36
41
|
perfil efetivo já injetado e trate \`GOVERN\` como fallback conservador quando ele for o padrão.
|
|
@@ -39,6 +44,7 @@ perfil efetivo já injetado e trate \`GOVERN\` como fallback conservador quando
|
|
|
39
44
|
Antes de editar, leia o **perfil efetivo** injetado pelo WendKeep e siga somente sua rota:
|
|
40
45
|
- \`OFF\`: não imponha processo Wend; a governança pertence ao **harness nativo da LLM**.
|
|
41
46
|
- \`FLOW\`: inicie o microcontrato com \`wendkeep flow start\` antes de editar os paths permitidos.
|
|
47
|
+
- \`delivery\`: inicie \`wendkeep delivery start\` antes das operações autorizadas; não crie change.
|
|
42
48
|
- \`GUIDE\`, \`GOVERN\` ou \`ASSURE\`: não edite código antes de Propose / \`wendkeep change new\`.
|
|
43
49
|
Este gate nunca transforma \`OFF\` ou \`FLOW\` silenciosamente em \`GOVERN\`.
|
|
44
50
|
</HARD-GATE>
|
|
@@ -47,14 +53,16 @@ Este gate nunca transforma \`OFF\` ou \`FLOW\` silenciosamente em \`GOVERN\`.
|
|
|
47
53
|
|
|
48
54
|
- **OFF — LLM nativa:** Wend Runtime desligado; esta skill devolve a execução ao harness nativo.
|
|
49
55
|
- **FLOW — E → V:** \`flow start\` → implementar com wk-tdd → \`flow finish\`; sem change/ADR/verdict.
|
|
50
|
-
- **GUIDE — P → E → V:** change
|
|
56
|
+
- **GUIDE — P → E → V:** change new --guide, sem design/spec/ADR automáticos quando não há impacto de contrato.
|
|
51
57
|
- **GOVERN — P → R → E → V:** loop a2 atual, com design/revisão; é o padrão conservador.
|
|
52
58
|
- **ASSURE — P → R → E → V → C:** GOVERN acrescido de confirmação e handoff explícitos.
|
|
53
59
|
|
|
54
60
|
## Passos para GUIDE, GOVERN e ASSURE
|
|
55
61
|
|
|
56
62
|
1. **Explore** — entenda o problema antes de propor. Leia o código/contexto relevante.
|
|
57
|
-
2. **Propose** — \`wendkeep change new <slug
|
|
63
|
+
2. **Propose** — GUIDE usa \`wendkeep change new <slug> --guide\`; GOVERN/ASSURE usam
|
|
64
|
+
\`wendkeep change new <slug>\`. O GUIDE compacto exige objetivo, critérios de aceite, áreas
|
|
65
|
+
afetadas, testes e resultado. GOVERN/ASSURE criam \`08-Mudanças/<slug>/\` com:
|
|
58
66
|
- \`proposta.md\` — *por quê* e *o que muda* (o WHAT).
|
|
59
67
|
- \`design.md\` — a abordagem técnica.
|
|
60
68
|
- \`tarefas.md\` — a lista de tarefas \`- [ ] N.N descrição\`.
|
|
@@ -83,7 +91,7 @@ Este gate nunca transforma \`OFF\` ou \`FLOW\` silenciosamente em \`GOVERN\`.
|
|
|
83
91
|
\`[req:]\`) recebe verdict automático — pula este passe.
|
|
84
92
|
6. **Archive** — \`wendkeep change archive <slug>\`. O *gate* exige sensores verdes **E**
|
|
85
93
|
\`verdict.json\` cobrindo os \`[req:]\`. Passando, promove os deltas pro \`07-Specs\`,
|
|
86
|
-
move a change pro \`_arquivo
|
|
94
|
+
move a change pro \`_arquivo\`; GUIDE com contract_impact none não gera ADR automático.
|
|
87
95
|
|
|
88
96
|
## Regras
|
|
89
97
|
|
|
@@ -296,10 +304,15 @@ persist it explicitly through \`profile use OFF\`.
|
|
|
296
304
|
|
|
297
305
|
- **FLOW:** a local, reversible, bounded adjustment with no contract/spec, security, dependency,
|
|
298
306
|
CI/release, or policy impact.
|
|
299
|
-
- **GUIDE:**
|
|
307
|
+
- **GUIDE:** compact implementation with a change, but no automatic spec/design/ADR when contract impact is none.
|
|
300
308
|
- **GOVERN:** the conservative choice when uncertain or risky, and for sensitive surfaces.
|
|
301
309
|
- **ASSURE:** GOVERN when explicit confirmation and handoff are part of the contract.
|
|
302
310
|
|
|
311
|
+
Also classify work kind as inspection, maintenance, implementation, delivery, or recovery.
|
|
312
|
+
Operational risk and contract impact are independent. Merge, push, tag, and publish for
|
|
313
|
+
already-approved code use delivery + ASSURE without another change; use delivery start and
|
|
314
|
+
delivery finish to produce a receipt.
|
|
315
|
+
|
|
303
316
|
The LLM harness owns semantic classification; Wend Runtime validates and applies the lease. If
|
|
304
317
|
there is no causally identified session or the command fails, do not fabricate state: use the
|
|
305
318
|
already injected effective profile, with \`GOVERN\` as the conservative configured fallback.
|
|
@@ -308,6 +321,7 @@ already injected effective profile, with \`GOVERN\` as the conservative configur
|
|
|
308
321
|
Before editing, read the injected **effective profile** and follow only its route:
|
|
309
322
|
- \`OFF\`: impose no Wend process; governance belongs to the **native LLM harness**.
|
|
310
323
|
- \`FLOW\`: start the microcontract with \`wendkeep flow start\` before editing allowed paths.
|
|
324
|
+
- \`delivery\`: run \`wendkeep delivery start\` before authorized operations; do not create a change.
|
|
311
325
|
- \`GUIDE\`, \`GOVERN\`, or \`ASSURE\`: do not edit code before Propose / \`wendkeep change new\`.
|
|
312
326
|
This gate never silently turns \`OFF\` or \`FLOW\` into \`GOVERN\`.
|
|
313
327
|
</HARD-GATE>
|
|
@@ -316,15 +330,16 @@ This gate never silently turns \`OFF\` or \`FLOW\` into \`GOVERN\`.
|
|
|
316
330
|
|
|
317
331
|
- **OFF — native LLM:** Wend Runtime is disabled; this skill returns execution to the native harness.
|
|
318
332
|
- **FLOW — E → V:** \`flow start\` → implement with wk-tdd → \`flow finish\`; no change/ADR/verdict.
|
|
319
|
-
- **GUIDE — P → E → V:**
|
|
333
|
+
- **GUIDE — P → E → V:** change new --guide, with no automatic design/spec/ADR when contract impact is none.
|
|
320
334
|
- **GOVERN — P → R → E → V:** the current a2 loop with design/review; the conservative default.
|
|
321
335
|
- **ASSURE — P → R → E → V → C:** GOVERN plus explicit confirmation and handoff.
|
|
322
336
|
|
|
323
337
|
## Steps for GUIDE, GOVERN, and ASSURE
|
|
324
338
|
|
|
325
339
|
1. **Explore** — understand the problem before proposing.
|
|
326
|
-
2. **Propose** — \`wendkeep change new <slug
|
|
327
|
-
|
|
340
|
+
2. **Propose** — GUIDE uses \`wendkeep change new <slug> --guide\`; GOVERN/ASSURE use
|
|
341
|
+
\`wendkeep change new <slug>\`. Compact GUIDE requires objective, acceptance criteria,
|
|
342
|
+
affected areas, tests, and result. The change becomes *current* through global
|
|
328
343
|
\`.brain/CURRENT_CHANGE.md\`. Multiple changes may stay open; hooks and \`change list/status\`
|
|
329
344
|
show every pending task, while commands without \`--change\` use only the current change.
|
|
330
345
|
Before implementation, resolve \`spec_impact\`: \`required\` needs the capability listed in
|
|
@@ -341,7 +356,8 @@ This gate never silently turns \`OFF\` or \`FLOW\` into \`GOVERN\`.
|
|
|
341
356
|
5. **Verify deep** — the **wk-verify** skill (fresh, author≠verifier) writes \`verdict.json\`.
|
|
342
357
|
A trivial change (no \`[req:]\`) gets an auto verdict.
|
|
343
358
|
6. **Archive** — \`wendkeep change archive <slug>\`. The gate needs green sensors AND a
|
|
344
|
-
verdict AND no open tasks. It promotes the delta into \`07-Specs
|
|
359
|
+
verdict AND no open tasks. It promotes the delta into \`07-Specs\`; compact GUIDE does not
|
|
360
|
+
mint an automatic ADR.
|
|
345
361
|
|
|
346
362
|
## Rules
|
|
347
363
|
- Multiple changes may stay open. \`CURRENT_CHANGE.md\` marks one current change without hiding
|
package/src/sync-defs.mjs
CHANGED
|
@@ -66,7 +66,7 @@ The persistent profile is selected explicitly; missing or invalid configuration
|
|
|
66
66
|
|
|
67
67
|
Before every implementation, the native LLM harness **MUST run the routing gate**:
|
|
68
68
|
1. Inspect \`wendkeep profile status\`.
|
|
69
|
-
2. Classify
|
|
69
|
+
2. Classify work kind, contract impact, and operation risk independently; then choose the profile.
|
|
70
70
|
3. Register the choice with
|
|
71
71
|
\`wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <text>\`.
|
|
72
72
|
4. Re-check \`wendkeep profile status\` and follow the effective profile before editing.
|
|
@@ -82,12 +82,15 @@ fallback when configuration is missing or invalid. The lease expires when the re
|
|
|
82
82
|
Route work by the effective profile:
|
|
83
83
|
- **OFF** — Wend Runtime is disabled and governance belongs to the native LLM harness; Keep Core stays active.
|
|
84
84
|
- **FLOW** — Execute → Validate through \`wendkeep flow start/finish\`, without creating a change.
|
|
85
|
-
- **GUIDE** — Plan → Execute → Validate through
|
|
85
|
+
- **GUIDE** — Plan → Execute → Validate through \`change new --guide\`; no automatic spec/design/ADR for contract_impact none.
|
|
86
86
|
- **GOVERN** — the default a2 loop: \`wendkeep change new <slug>\` → review → implement tasks test-first
|
|
87
87
|
(tag proof \`[sensor:id]\` and requirement \`[req:ID]\`) → \`wendkeep verify\` →
|
|
88
88
|
\`wendkeep verify --deep\` + independent read-only verdict → \`wendkeep change archive\`.
|
|
89
89
|
- **ASSURE** — GOVERN plus explicit confirmation and handoff.
|
|
90
90
|
|
|
91
|
+
Delivery of already-approved behavior uses \`wendkeep delivery start/status/finish/abandon\` with
|
|
92
|
+
ASSURE authorization and an append-only receipt. It does not create a new change, spec, or ADR.
|
|
93
|
+
|
|
91
94
|
Inspect with \`wendkeep profile status\` / \`wendkeep change status\` /
|
|
92
95
|
\`spec effective --change <slug>\` / \`sensors list\`. Author specs only in
|
|
93
96
|
\`08-Mudanças/<slug>/specs/\`; \`07-Specs\` is generated and must not be edited directly.
|
package/src/sync.mjs
CHANGED
|
@@ -81,11 +81,11 @@ export async function runSync(argv) {
|
|
|
81
81
|
// código é propagado sem ser tratado como falha da cadeia.
|
|
82
82
|
step(3, 'doctor');
|
|
83
83
|
const { runDoctor } = await import('./doctor.mjs');
|
|
84
|
-
const doctorCode = runDoctor(['--vault', vaultBase, '--project', projectPath]);
|
|
84
|
+
const doctorCode = runDoctor(['--vault', vaultBase, '--project', projectPath, '--scope', 'core']);
|
|
85
85
|
|
|
86
86
|
// Nunca afirmar "tudo em dia": o doctor sai 0 mesmo tendo listado órfãos, seções
|
|
87
87
|
// desatualizadas ou modelos sem preço — essas checagens não são fatais. Uma linha final
|
|
88
88
|
// otimista contradiria o relatório logo acima dela.
|
|
89
|
-
process.stdout.write(`\nwendkeep sync:
|
|
89
|
+
process.stdout.write(`\nwendkeep sync: ${doctorCode ? 'falhou — Keep Core comprometido' : 'concluído — Keep Core saudável'}\n`);
|
|
90
90
|
return doctorCode;
|
|
91
91
|
}
|