wendkeep 0.80.2 → 0.85.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 +103 -0
- package/README.en.md +28 -11
- package/README.md +28 -11
- package/docs/en/commands/capabilities.md +82 -0
- package/docs/en/commands/getting-started.md +3 -1
- package/docs/en/commands/mcp.md +99 -0
- package/docs/en/commands/portable.md +88 -0
- package/docs/en/commands/sync-protocol.md +58 -0
- package/docs/en/commands/tdd.md +96 -0
- package/docs/en/commands/verify.md +5 -0
- package/docs/pt-BR/commands/capabilities.md +82 -0
- package/docs/pt-BR/commands/getting-started.md +3 -2
- package/docs/pt-BR/commands/mcp.md +99 -0
- package/docs/pt-BR/commands/portable.md +87 -0
- package/docs/pt-BR/commands/sync-protocol.md +58 -0
- package/docs/pt-BR/commands/tdd.md +96 -0
- package/docs/pt-BR/commands/verify.md +5 -0
- package/hooks/active-context-store.mjs +2 -0
- package/hooks/change-core.mjs +5 -0
- package/hooks/project-scope.mjs +2 -1
- package/hooks/session-ensure.mjs +23 -7
- package/hooks/session-start.mjs +20 -5
- package/package.json +3 -3
- package/packages/cli/src/index.mjs +42 -2
- package/packages/harness/src/sensors-core.mjs +16 -3
- package/packages/integrations/src/capabilities.mjs +220 -0
- package/packages/integrations/src/index.mjs +1 -0
- package/packages/mcp/src/audit.mjs +49 -0
- package/packages/mcp/src/cli.mjs +78 -0
- package/packages/mcp/src/config.mjs +22 -1
- package/packages/mcp/src/effects.mjs +115 -0
- package/packages/mcp/src/executor.mjs +354 -0
- package/packages/mcp/src/index.mjs +7 -0
- package/packages/mcp/src/server.mjs +342 -0
- package/packages/mcp/src/stdio.mjs +38 -0
- package/packages/mcp/src/sync.mjs +56 -0
- package/packages/pi/package.json +2 -1
- package/packages/pi/src/index.mjs +29 -0
- package/schema/handoff-contract-v1.schema.json +4 -0
- package/schema/host-capability-manifest-v1.schema.json +46 -0
- package/schema/host-coverage-v1.schema.json +55 -0
- package/schema/mcp-effect-manifest-v1.schema.json +36 -0
- package/schema/mcp-tool-input-v1.schema.json +32 -0
- package/schema/mcp-tool-result-v1.schema.json +22 -0
- package/schema/portable-active-work-v1.schema.json +38 -0
- package/schema/portable-state-v1.schema.json +36 -0
- package/schema/sync-event-v1.schema.json +25 -0
- package/schema/sync-private-envelope-v1.schema.json +16 -0
- package/schema/sync-state-v1.schema.json +18 -0
- package/schema/task-contract-v1.schema.json +2 -0
- package/schema/tdd-attestation-v1.schema.json +39 -0
- package/schema/wendkeep.evidence-envelope-v2.schema.json +17 -0
- package/schema/wendkeep.sensors.schema.json +19 -0
- package/src/active-context-runtime.mjs +1 -0
- package/src/capabilities.mjs +50 -0
- package/src/doctor.mjs +28 -0
- package/src/evidence-envelope.mjs +12 -6
- package/src/host-capabilities.mjs +34 -0
- package/src/init.mjs +3 -3
- package/src/mcp.mjs +7 -0
- package/src/observer-snapshot.mjs +25 -0
- package/src/portable.mjs +558 -0
- package/src/skills-seed.mjs +26 -0
- package/src/sync-adapters.mjs +188 -0
- package/src/sync-outbox.mjs +155 -0
- package/src/sync-protocol-cli.mjs +277 -0
- package/src/sync-protocol.mjs +368 -0
- package/src/sync.mjs +8 -0
- package/src/task-contracts.mjs +67 -2
- package/src/task.mjs +5 -1
- package/src/tdd-attestation-store.mjs +98 -0
- package/src/tdd-attestation.mjs +254 -0
- package/src/tdd.mjs +198 -0
- package/src/vault-readme.mjs +4 -4
- package/src/verify.mjs +24 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { sanitizeMemoryText } from '../packages/vault/src/memory-schema.mjs';
|
|
4
|
+
|
|
5
|
+
const OUTPUT_TAIL_LIMIT = 2_000;
|
|
6
|
+
const COMMAND_LIMIT = 1_000;
|
|
7
|
+
const IDENTITY_FIELDS = [
|
|
8
|
+
'project_id', 'repository_id', 'worktree_id', 'work_session_id', 'change_slug',
|
|
9
|
+
];
|
|
10
|
+
const INFRASTRUCTURE_FAILURES = [
|
|
11
|
+
/cannot find module/i,
|
|
12
|
+
/module not found/i,
|
|
13
|
+
/cannot find package/i,
|
|
14
|
+
/syntaxerror/i,
|
|
15
|
+
/enoent/i,
|
|
16
|
+
/command not found/i,
|
|
17
|
+
/is not recognized as an internal or external command/i,
|
|
18
|
+
/failed to load (?:config|configuration)/i,
|
|
19
|
+
/configuration (?:error|invalid|missing)/i,
|
|
20
|
+
/unknown (?:option|argument)/i,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function stableValue(value) {
|
|
24
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
25
|
+
if (!value || typeof value !== 'object') return value;
|
|
26
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sha256(value) {
|
|
30
|
+
return `sha256:${createHash('sha256').update(String(value || ''), 'utf8').digest('hex')}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function canonicalId(value) {
|
|
34
|
+
return createHash('sha256').update(JSON.stringify(stableValue(value)), 'utf8').digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function uniqueStrings(values) {
|
|
38
|
+
const input = Array.isArray(values) ? values : [values];
|
|
39
|
+
return [...new Set(input.map((value) => String(value || '').trim()).filter(Boolean))];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizedPath(value) {
|
|
43
|
+
const path = String(value || '').replaceAll('\\', '/').replace(/^\.\//, '').trim();
|
|
44
|
+
if (!path || path.startsWith('/') || /^[A-Za-z]:\//.test(path) || path.split('/').includes('..')) {
|
|
45
|
+
throw Object.assign(new Error(`invalid test path: ${value}`), { code: 'TDD_TEST_PATH_INVALID' });
|
|
46
|
+
}
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function testPathsOf(values) {
|
|
51
|
+
return uniqueStrings(values).map(normalizedPath).sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sanitizeDiagnostic(value) {
|
|
55
|
+
return sanitizeMemoryText(String(value || ''))
|
|
56
|
+
.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')
|
|
57
|
+
.replace(/file:\/\/\/[A-Za-z]:\/[^\s)]+/gi, '[LOCAL_FILE]')
|
|
58
|
+
.replace(/"[A-Za-z]:\\[^"\r\n]+"/g, '"[LOCAL_EXECUTABLE]"')
|
|
59
|
+
.replace(/'[A-Za-z]:\\+[^'\r\n]+'/g, "'[LOCAL_FILE]'")
|
|
60
|
+
.replace(/\b[A-Za-z]:\\+[^\s)"'\r\n]+/g, '[LOCAL_FILE]')
|
|
61
|
+
.replace(/\/[Uu]sers\/[^/\s]+\/[^\s)]+/g, '[LOCAL_FILE]')
|
|
62
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9_]{12,}\b/g, '[REDACTED_SECRET]')
|
|
63
|
+
.replace(/\bxox[baprs]-[A-Za-z0-9-]{12,}\b/g, '[REDACTED_SECRET]')
|
|
64
|
+
.replace(/:\/\/([^:\s/@]+):([^@\s/]+)@/g, '://[REDACTED_SECRET]@')
|
|
65
|
+
.replace(/\r/g, '')
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function boundedTail(value) {
|
|
70
|
+
const sanitized = sanitizeDiagnostic(value);
|
|
71
|
+
return sanitized.length <= OUTPUT_TAIL_LIMIT
|
|
72
|
+
? sanitized
|
|
73
|
+
: `…${sanitized.slice(-(OUTPUT_TAIL_LIMIT - 1))}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function commandObservation(command, result, observedAt) {
|
|
77
|
+
const rawOutput = [result?.stdout, result?.stderr, result?.error?.message].filter(Boolean).join('\n');
|
|
78
|
+
return {
|
|
79
|
+
command: sanitizeDiagnostic(command).slice(0, COMMAND_LIMIT),
|
|
80
|
+
command_digest: sha256(command),
|
|
81
|
+
exit_code: Number.isInteger(result?.status) ? result.status : null,
|
|
82
|
+
output_digest: sha256(rawOutput),
|
|
83
|
+
output_tail: boundedTail(rawOutput),
|
|
84
|
+
observed_at: String(observedAt || new Date().toISOString()),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function snapshotObservation(snapshot = {}) {
|
|
89
|
+
return {
|
|
90
|
+
branch: String(snapshot.branch || ''),
|
|
91
|
+
head_sha: String(snapshot.head_sha || ''),
|
|
92
|
+
index_tree_sha: String(snapshot.index_tree_sha || ''),
|
|
93
|
+
worktree_digest: String(snapshot.worktree_digest || ''),
|
|
94
|
+
change_manifest: stableValue(snapshot.change_manifest || {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function attestationIdentity({ identity = {}, taskId, requirementId }) {
|
|
99
|
+
const normalizedIdentity = Object.fromEntries(
|
|
100
|
+
IDENTITY_FIELDS.map((field) => [field, String(identity[field] || '').trim()]),
|
|
101
|
+
);
|
|
102
|
+
if (Object.values(normalizedIdentity).some((value) => !value)
|
|
103
|
+
|| !String(taskId || '').trim() || !String(requirementId || '').trim()) {
|
|
104
|
+
throw Object.assign(new Error('causal identity, task and requirement are required'), {
|
|
105
|
+
code: 'TDD_IDENTITY_REQUIRED',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...normalizedIdentity,
|
|
110
|
+
task_id: String(taskId).trim(),
|
|
111
|
+
requirement_id: String(requirementId).trim(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function baseAttestation(input) {
|
|
116
|
+
const causal = attestationIdentity(input);
|
|
117
|
+
return {
|
|
118
|
+
schema_version: 1,
|
|
119
|
+
attestation_id: canonicalId(causal),
|
|
120
|
+
...causal,
|
|
121
|
+
profile: String(input.profile || 'OFF').toUpperCase(),
|
|
122
|
+
state: 'invalid',
|
|
123
|
+
test_paths: testPathsOf(input.testPaths),
|
|
124
|
+
red: null,
|
|
125
|
+
green: null,
|
|
126
|
+
green_history: [],
|
|
127
|
+
waiver: null,
|
|
128
|
+
review_flags: [],
|
|
129
|
+
invalid_reason: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function invalid(attestation, reason) {
|
|
134
|
+
return { ...attestation, state: 'invalid', invalid_reason: reason };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function classifyTddRedResult(result = {}) {
|
|
138
|
+
if ((result.status ?? 1) === 0) return { valid: false, reason: 'TDD_RED_ALREADY_GREEN' };
|
|
139
|
+
const diagnostic = [result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n');
|
|
140
|
+
if (INFRASTRUCTURE_FAILURES.some((pattern) => pattern.test(diagnostic))) {
|
|
141
|
+
return { valid: false, reason: 'TDD_RED_INFRASTRUCTURE_FAILURE' };
|
|
142
|
+
}
|
|
143
|
+
return { valid: true, reason: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createRedAttestation(input = {}) {
|
|
147
|
+
const base = baseAttestation(input);
|
|
148
|
+
const execution = commandObservation(input.command, input.result, input.observedAt);
|
|
149
|
+
const red = {
|
|
150
|
+
...snapshotObservation(input.snapshot),
|
|
151
|
+
...execution,
|
|
152
|
+
failure_digest: execution.output_digest,
|
|
153
|
+
};
|
|
154
|
+
delete red.output_digest;
|
|
155
|
+
const observed = { ...base, red };
|
|
156
|
+
const classification = classifyTddRedResult(input.result);
|
|
157
|
+
return classification.valid
|
|
158
|
+
? { ...observed, state: 'red-observed', invalid_reason: null }
|
|
159
|
+
: invalid(observed, classification.reason);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sameIdentity(attestation, input) {
|
|
163
|
+
return IDENTITY_FIELDS.every((field) => (
|
|
164
|
+
String(attestation?.[field] || '') === String(input.identity?.[field] || '')
|
|
165
|
+
))
|
|
166
|
+
&& String(attestation?.task_id || '') === String(input.taskId || '')
|
|
167
|
+
&& String(attestation?.requirement_id || '') === String(input.requirementId || '');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function changedManifestPaths(left = {}, right = {}) {
|
|
171
|
+
const paths = new Set([...Object.keys(left || {}), ...Object.keys(right || {})]);
|
|
172
|
+
return [...paths].filter((path) => String(left?.[path] || '') !== String(right?.[path] || '')).sort();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isDeclaredTestPath(path, declared) {
|
|
176
|
+
const normalized = normalizedPath(path);
|
|
177
|
+
return declared.some((testPath) => normalized === testPath || normalized.startsWith(`${testPath}/`));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function completeGreenAttestation(attestation, input = {}) {
|
|
181
|
+
const current = { ...attestation, green_history: [...(attestation?.green_history || [])] };
|
|
182
|
+
if (!sameIdentity(current, input)
|
|
183
|
+
|| String(current.red?.branch || '') !== String(input.snapshot?.branch || '')) {
|
|
184
|
+
return invalid(current, 'TDD_CAUSAL_IDENTITY_MISMATCH');
|
|
185
|
+
}
|
|
186
|
+
if (!current.red || !['red-observed', 'green-observed'].includes(String(current.state || ''))) {
|
|
187
|
+
return invalid(current, 'TDD_RED_REQUIRED');
|
|
188
|
+
}
|
|
189
|
+
if ((input.result?.status ?? 1) !== 0) return invalid(current, 'TDD_GREEN_FAILED');
|
|
190
|
+
if (input.isAncestor !== true) return invalid(current, 'TDD_GREEN_NOT_CAUSAL_SUCCESSOR');
|
|
191
|
+
|
|
192
|
+
const nextTestPaths = testPathsOf(input.testPaths?.length ? input.testPaths : current.test_paths);
|
|
193
|
+
const allTestPaths = uniqueStrings([...current.test_paths, ...nextTestPaths]).map(normalizedPath);
|
|
194
|
+
const changedPaths = uniqueStrings([
|
|
195
|
+
...changedManifestPaths(current.red.change_manifest, input.snapshot?.change_manifest),
|
|
196
|
+
...(input.committedPaths || []),
|
|
197
|
+
]).map(normalizedPath).sort();
|
|
198
|
+
const productionPaths = changedPaths.filter((path) => !isDeclaredTestPath(path, allTestPaths));
|
|
199
|
+
if (!productionPaths.length) return invalid(current, 'TDD_IMPLEMENTATION_NOT_AFTER_RED');
|
|
200
|
+
|
|
201
|
+
const execution = commandObservation(input.command, input.result, input.observedAt);
|
|
202
|
+
const green = {
|
|
203
|
+
...snapshotObservation(input.snapshot),
|
|
204
|
+
...execution,
|
|
205
|
+
result_digest: execution.output_digest,
|
|
206
|
+
production_paths: productionPaths,
|
|
207
|
+
};
|
|
208
|
+
delete green.output_digest;
|
|
209
|
+
if (current.green) current.green_history.push(current.green);
|
|
210
|
+
const testPathsChanged = JSON.stringify(current.test_paths) !== JSON.stringify(nextTestPaths);
|
|
211
|
+
return {
|
|
212
|
+
...current,
|
|
213
|
+
state: 'green-observed',
|
|
214
|
+
invalid_reason: null,
|
|
215
|
+
test_paths: nextTestPaths,
|
|
216
|
+
green,
|
|
217
|
+
green_history: current.green_history,
|
|
218
|
+
review_flags: testPathsChanged ? ['TDD_TEST_PATHS_CHANGED'] : [],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function evaluateTddAttestation(attestation, snapshot = {}, options = {}) {
|
|
223
|
+
if (!attestation || !['green-observed', 'waived'].includes(attestation.state)) {
|
|
224
|
+
return attestation || { state: 'invalid', invalid_reason: 'TDD_ATTESTATION_MISSING' };
|
|
225
|
+
}
|
|
226
|
+
if (attestation.state === 'waived') return attestation;
|
|
227
|
+
if (Array.isArray(options.mutationSurvivors) && options.mutationSurvivors.length) {
|
|
228
|
+
return invalid(attestation, 'TDD_MUTATION_SURVIVOR');
|
|
229
|
+
}
|
|
230
|
+
const green = attestation.green || {};
|
|
231
|
+
const stale = ['head_sha', 'index_tree_sha', 'worktree_digest']
|
|
232
|
+
.some((field) => String(green[field] || '') !== String(snapshot[field] || ''));
|
|
233
|
+
return stale ? invalid(attestation, 'TDD_GREEN_STALE_AFTER_REFACTOR') : attestation;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function waiveTddAttestation(input = {}) {
|
|
237
|
+
const authority = sanitizeDiagnostic(input.authority);
|
|
238
|
+
const reason = sanitizeDiagnostic(input.reason);
|
|
239
|
+
if (!authority || !reason) {
|
|
240
|
+
throw Object.assign(new Error('waiver requires explicit human authority and reason'), {
|
|
241
|
+
code: 'TDD_WAIVER_AUTHORITY_REQUIRED',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
...baseAttestation(input),
|
|
246
|
+
state: 'waived',
|
|
247
|
+
invalid_reason: null,
|
|
248
|
+
waiver: {
|
|
249
|
+
authority: authority.slice(0, 200),
|
|
250
|
+
reason: reason.slice(0, 1_000),
|
|
251
|
+
observed_at: String(input.observedAt || new Date().toISOString()),
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
package/src/tdd.mjs
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
import { activeChange } from '../hooks/change-core.mjs';
|
|
5
|
+
import { resolveActiveContext } from '../hooks/active-context-store.mjs';
|
|
6
|
+
import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
|
|
7
|
+
import { findProjectRoot } from '../packages/harness/src/sensors-core.mjs';
|
|
8
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
9
|
+
import { buildTaskContractSnapshot } from './task-contracts.mjs';
|
|
10
|
+
import {
|
|
11
|
+
completeGreenAttestation,
|
|
12
|
+
createRedAttestation,
|
|
13
|
+
evaluateTddAttestation,
|
|
14
|
+
waiveTddAttestation,
|
|
15
|
+
} from './tdd-attestation.mjs';
|
|
16
|
+
import {
|
|
17
|
+
captureTddSnapshot,
|
|
18
|
+
committedPathsBetween,
|
|
19
|
+
isGitAncestor,
|
|
20
|
+
readTddAttestationStore,
|
|
21
|
+
saveTddAttestation,
|
|
22
|
+
} from './tdd-attestation-store.mjs';
|
|
23
|
+
|
|
24
|
+
const HELP = `wendkeep tdd <red|green|status|waive> <task-id>
|
|
25
|
+
|
|
26
|
+
--requirement <id> requirement bound to the task
|
|
27
|
+
--test <path> test path (repeatable)
|
|
28
|
+
--command <command> discriminating test command
|
|
29
|
+
--reason <text> waiver reason
|
|
30
|
+
--authority <id> explicit human waiver authority
|
|
31
|
+
--session <id> causal work session
|
|
32
|
+
--change <slug> active change override
|
|
33
|
+
--json emit structured JSON
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
function opt(argv, name) {
|
|
37
|
+
const index = argv.indexOf(name);
|
|
38
|
+
return index >= 0 ? argv[index + 1] || '' : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function opts(argv, name) {
|
|
42
|
+
const values = [];
|
|
43
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
44
|
+
if (argv[index] === name && argv[index + 1]) values.push(argv[index + 1]);
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tddError(code, message) {
|
|
50
|
+
return Object.assign(new Error(message), { code });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function commandState(argv) {
|
|
54
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
55
|
+
if (!vaultRaw) throw tddError('TDD_VAULT_MISSING', 'no vault (--vault or OBSIDIAN_VAULT_PATH)');
|
|
56
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
57
|
+
const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
|
|
58
|
+
const requestedSession = opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '';
|
|
59
|
+
const identity = resolveCommandActiveContext({
|
|
60
|
+
vaultBase, projectRoot, sessionId: requestedSession, requireExisting: true,
|
|
61
|
+
});
|
|
62
|
+
if (!identity) throw tddError('TDD_ACTIVE_CONTEXT_NOT_FOUND', 'active context is required');
|
|
63
|
+
const context = resolveActiveContext(vaultBase, identity);
|
|
64
|
+
const explicitChange = opt(argv, '--change');
|
|
65
|
+
const changeSlug = explicitChange || activeChange(vaultBase, { context: identity });
|
|
66
|
+
if (!changeSlug) throw tddError('TDD_CHANGE_NOT_FOUND', 'no active change');
|
|
67
|
+
if (explicitChange && context.change_slug && explicitChange !== context.change_slug) {
|
|
68
|
+
throw tddError('TDD_CHANGE_CONTEXT_MISMATCH', 'requested change differs from active context');
|
|
69
|
+
}
|
|
70
|
+
const runtime = resolveHookOperatingProfile({
|
|
71
|
+
input: { cwd: projectRoot, session_id: identity.sessionId || requestedSession },
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
vaultBase, projectRoot, identity, context, changeSlug,
|
|
75
|
+
profile: runtime.profile,
|
|
76
|
+
json: argv.includes('--json'),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function causalIdentity(state) {
|
|
81
|
+
return {
|
|
82
|
+
project_id: state.identity.projectId,
|
|
83
|
+
repository_id: state.identity.repositoryId,
|
|
84
|
+
worktree_id: state.identity.worktreeId,
|
|
85
|
+
work_session_id: state.identity.workSessionId,
|
|
86
|
+
change_slug: state.changeSlug,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function taskAndRequirement(state, taskId, requested = '') {
|
|
91
|
+
const snapshot = buildTaskContractSnapshot({ ...state, profile: state.profile });
|
|
92
|
+
const task = snapshot.contracts.find((item) => item.task_id === taskId);
|
|
93
|
+
if (!task) throw tddError('TDD_TASK_NOT_FOUND', `task not found: ${taskId || '(missing id)'}`);
|
|
94
|
+
const requirementId = String(requested || task.requirement_ids[0] || '').trim();
|
|
95
|
+
if (!requirementId || !task.requirement_ids.includes(requirementId)) {
|
|
96
|
+
throw tddError('TDD_REQUIREMENT_NOT_BOUND', `requirement is not bound to task ${taskId}`);
|
|
97
|
+
}
|
|
98
|
+
return { task, requirementId };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runObserved(command, projectRoot) {
|
|
102
|
+
if (!String(command || '').trim()) throw tddError('TDD_COMMAND_REQUIRED', '--command is required');
|
|
103
|
+
const env = { ...process.env };
|
|
104
|
+
// A test command launched by the CLI is a new observation, not a recursive
|
|
105
|
+
// child of WendKeep's own node:test process.
|
|
106
|
+
delete env.NODE_TEST_CONTEXT;
|
|
107
|
+
return spawnSync(command, [], {
|
|
108
|
+
cwd: projectRoot,
|
|
109
|
+
shell: true,
|
|
110
|
+
encoding: 'utf8',
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
113
|
+
env,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function currentAttestation(state, taskId) {
|
|
118
|
+
return readTddAttestationStore(state.vaultBase, state.changeSlug).attestations
|
|
119
|
+
.find((item) => item.task_id === taskId) || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function write(value, json) {
|
|
123
|
+
process.stdout.write(json ? `${JSON.stringify(value)}\n` : `${value.task_id}: ${value.state}\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fail(error, json) {
|
|
127
|
+
const payload = { ok: false, code: error?.code || 'TDD_COMMAND_FAILED', error: String(error?.message || error) };
|
|
128
|
+
process.stderr.write(json ? `${JSON.stringify(payload)}\n` : `wendkeep tdd: ${payload.code}: ${payload.error}\n`);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function runTdd(argv = []) {
|
|
133
|
+
const sub = argv[0];
|
|
134
|
+
if (!sub || ['help', '--help', '-h'].includes(sub)) {
|
|
135
|
+
process.stdout.write(HELP);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
const json = argv.includes('--json');
|
|
139
|
+
try {
|
|
140
|
+
const state = commandState(argv);
|
|
141
|
+
const taskId = String(argv[1] || '').trim();
|
|
142
|
+
if (!taskId) throw tddError('TDD_TASK_REQUIRED', 'task id is required');
|
|
143
|
+
|
|
144
|
+
if (sub === 'red') {
|
|
145
|
+
const { requirementId } = taskAndRequirement(state, taskId, opt(argv, '--requirement'));
|
|
146
|
+
const testPaths = opts(argv, '--test');
|
|
147
|
+
if (!testPaths.length) throw tddError('TDD_TEST_PATH_REQUIRED', 'at least one --test is required');
|
|
148
|
+
const command = opt(argv, '--command');
|
|
149
|
+
const attestation = createRedAttestation({
|
|
150
|
+
identity: causalIdentity(state), taskId, requirementId, testPaths,
|
|
151
|
+
profile: state.profile, command, result: runObserved(command, state.projectRoot),
|
|
152
|
+
snapshot: captureTddSnapshot(state.projectRoot),
|
|
153
|
+
});
|
|
154
|
+
saveTddAttestation(state.vaultBase, state.changeSlug, attestation);
|
|
155
|
+
write(attestation, state.json);
|
|
156
|
+
return attestation.state === 'red-observed' ? 0 : 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (sub === 'green') {
|
|
160
|
+
const current = currentAttestation(state, taskId);
|
|
161
|
+
if (!current) throw tddError('TDD_RED_REQUIRED', 'no RED attestation exists for this task');
|
|
162
|
+
const command = opt(argv, '--command');
|
|
163
|
+
const snapshot = captureTddSnapshot(state.projectRoot);
|
|
164
|
+
const attestation = completeGreenAttestation(current, {
|
|
165
|
+
identity: causalIdentity(state), taskId, requirementId: current.requirement_id,
|
|
166
|
+
testPaths: opts(argv, '--test'), command,
|
|
167
|
+
result: runObserved(command, state.projectRoot), snapshot,
|
|
168
|
+
isAncestor: isGitAncestor(state.projectRoot, current.red?.head_sha, snapshot.head_sha),
|
|
169
|
+
committedPaths: committedPathsBetween(state.projectRoot, current.red?.head_sha, snapshot.head_sha),
|
|
170
|
+
});
|
|
171
|
+
saveTddAttestation(state.vaultBase, state.changeSlug, attestation);
|
|
172
|
+
write(attestation, state.json);
|
|
173
|
+
return attestation.state === 'green-observed' ? 0 : 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (sub === 'waive') {
|
|
177
|
+
const { requirementId } = taskAndRequirement(state, taskId, opt(argv, '--requirement'));
|
|
178
|
+
const attestation = waiveTddAttestation({
|
|
179
|
+
identity: causalIdentity(state), taskId, requirementId, profile: state.profile,
|
|
180
|
+
reason: opt(argv, '--reason'), authority: opt(argv, '--authority'),
|
|
181
|
+
});
|
|
182
|
+
saveTddAttestation(state.vaultBase, state.changeSlug, attestation);
|
|
183
|
+
write(attestation, state.json);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (sub === 'status') {
|
|
188
|
+
const current = currentAttestation(state, taskId);
|
|
189
|
+
if (!current) throw tddError('TDD_ATTESTATION_NOT_FOUND', `no attestation for task ${taskId}`);
|
|
190
|
+
const evaluated = evaluateTddAttestation(current, captureTddSnapshot(state.projectRoot));
|
|
191
|
+
write(evaluated, state.json);
|
|
192
|
+
return ['green-observed', 'waived'].includes(evaluated.state) ? 0 : 1;
|
|
193
|
+
}
|
|
194
|
+
throw tddError('TDD_SUBCOMMAND_UNKNOWN', `unknown subcommand: ${sub}`);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return fail(error, json);
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/vault-readme.mjs
CHANGED
|
@@ -49,9 +49,9 @@ export function renderVaultReadme({ projectName, vaultPath, withMcp = true, loca
|
|
|
49
49
|
const table = [en ? '| Folder | Contents |' : '| Pasta | Conteúdo |', '| --- | --- |', ...rows].join('\n');
|
|
50
50
|
|
|
51
51
|
if (en) {
|
|
52
|
-
const mcpIntro = withMcp ? ', and
|
|
52
|
+
const mcpIntro = withMcp ? ', and queried through the native semantic **WendKeep MCP** server' : '';
|
|
53
53
|
const access = [`- **Obsidian:** open this folder with "Open folder as vault" → \`${vaultPath}\``];
|
|
54
|
-
if (withMcp) access.push('- **Agent (MCP):** the `wendkeep-vault` server
|
|
54
|
+
if (withMcp) access.push('- **Agent (MCP):** the native `wendkeep-vault` server points at this vault (set in `.mcp.json`), exposing bounded semantic reads and capability-gated writes without arbitrary filesystem access.');
|
|
55
55
|
access.push('- **Hooks:** Codex and Claude Code call `npx --no-install wendkeep hook <name>`; the vault is discovered from the project-local `.wendkeep.json` binding and checked against `.brain/PROJECT.json`.');
|
|
56
56
|
return `# Obsidian vault — ${name}
|
|
57
57
|
|
|
@@ -81,9 +81,9 @@ ${access.join('\n')}
|
|
|
81
81
|
`;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const mcpIntro = withMcp ? ', e
|
|
84
|
+
const mcpIntro = withMcp ? ', e consultada pelo servidor semântico nativo **WendKeep MCP**' : '';
|
|
85
85
|
const access = [`- **Obsidian:** abra esta pasta com "Open folder as vault" → \`${vaultPath}\``];
|
|
86
|
-
if (withMcp) access.push('- **Agente (MCP):** o servidor `wendkeep-vault`
|
|
86
|
+
if (withMcp) access.push('- **Agente (MCP):** o servidor nativo `wendkeep-vault` aponta para este vault pelo `wendkeep init` (em `.mcp.json`), expondo leituras semânticas bounded e writes por capability, sem acesso arbitrário ao filesystem.');
|
|
87
87
|
access.push('- **Hooks:** Codex e Claude Code chamam `npx --no-install wendkeep hook <name>`; o vault é descoberto pelo vínculo local `.wendkeep.json` e validado contra `.brain/PROJECT.json`.');
|
|
88
88
|
return `# Vault Obsidian — ${name}
|
|
89
89
|
|
package/src/verify.mjs
CHANGED
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
import { addLesson } from '../hooks/lessons-core.mjs';
|
|
23
23
|
import { getLocale } from '../hooks/locale.mjs';
|
|
24
24
|
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
25
|
+
import { resolveHookOperatingProfile } from '../hooks/operating-profile-runtime.mjs';
|
|
26
|
+
import { evaluateTddAttestation } from './tdd-attestation.mjs';
|
|
27
|
+
import { readTddAttestationStore } from './tdd-attestation-store.mjs';
|
|
25
28
|
import { writeVaultFileAtomic } from '../packages/vault/src/vault-path-safety.mjs';
|
|
29
|
+
import { evaluateHostCoverage } from '../packages/integrations/src/capabilities.mjs';
|
|
26
30
|
import { evidenceCheckoutBinding } from '../packages/vault/src/evidence-envelope.mjs';
|
|
27
31
|
import {
|
|
28
32
|
assertStableHead,
|
|
@@ -103,6 +107,16 @@ export function runVerify(argv) {
|
|
|
103
107
|
process.stderr.write(`wendkeep verify: wendkeep.sensors.json não encontrado em ${loaded.path} — rode da raiz do projeto ou use --project <raiz>\n`);
|
|
104
108
|
}
|
|
105
109
|
const sensors = loaded.sensors;
|
|
110
|
+
const hostGate = evaluateHostCoverage(
|
|
111
|
+
commandContext?.hostCoverage || null,
|
|
112
|
+
loaded.requiredHostCapabilities,
|
|
113
|
+
{ waivers: loaded.hostCapabilityWaivers },
|
|
114
|
+
);
|
|
115
|
+
if (!hostGate.ok) {
|
|
116
|
+
const missing = hostGate.findings.map((item) => `${item.capability}:${item.state}`).join(', ');
|
|
117
|
+
process.stderr.write(`wendkeep verify: HOST_CAPABILITY_UNAVAILABLE: ${missing}\n`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
106
120
|
const reqIds = [...new Set(tasks.flatMap((task) => task.reqs ?? []))];
|
|
107
121
|
const effective = buildEffectiveRequirementPackage(vaultBase, changeDir, reqIds);
|
|
108
122
|
const tasksHash = tasksHashOf(tarefas);
|
|
@@ -126,6 +140,9 @@ export function runVerify(argv) {
|
|
|
126
140
|
cwd: projectRoot,
|
|
127
141
|
env: sensorProcessEnv(vaultBase),
|
|
128
142
|
});
|
|
143
|
+
const mutationSurvivors = evidence.flatMap((sensor) => sensor.survivors ?? []);
|
|
144
|
+
const tddAttestations = readTddAttestationStore(vaultBase, slug).attestations
|
|
145
|
+
.map((attestation) => evaluateTddAttestation(attestation, startSnapshot, { mutationSurvivors }));
|
|
129
146
|
let finishSnapshot;
|
|
130
147
|
try {
|
|
131
148
|
finishSnapshot = captureGitSnapshot(projectRoot);
|
|
@@ -142,8 +159,10 @@ export function runVerify(argv) {
|
|
|
142
159
|
effectiveSpecSha256: `sha256:${effective.hash}`,
|
|
143
160
|
sensorConfigSha256: sensorConfigSha256(sensors, ids),
|
|
144
161
|
sensors: evidence,
|
|
162
|
+
tddAttestations,
|
|
145
163
|
startedAt,
|
|
146
164
|
finishedAt: new Date().toISOString(),
|
|
165
|
+
hostCoverage: commandContext?.hostCoverage || null,
|
|
147
166
|
});
|
|
148
167
|
writeAuthority('evidencia.json', `${JSON.stringify(envelope, null, 2)}\n`);
|
|
149
168
|
// Freshness seal: bind this evidence to the tarefas.md it was produced against, so the archive
|
|
@@ -195,11 +214,15 @@ export function runVerify(argv) {
|
|
|
195
214
|
// verify can announce success or assemble the deep package. Legacy projects without an active
|
|
196
215
|
// context preserve their pre-contract behavior until they migrate.
|
|
197
216
|
if (commandContext) {
|
|
217
|
+
const profileRuntime = resolveHookOperatingProfile({
|
|
218
|
+
input: { cwd: projectRoot, session_id: commandContext.sessionId || requestedSession },
|
|
219
|
+
});
|
|
198
220
|
const taskSnapshot = buildTaskContractSnapshot({
|
|
199
221
|
vaultBase,
|
|
200
222
|
projectRoot,
|
|
201
223
|
changeSlug: slug,
|
|
202
224
|
identity: commandContext,
|
|
225
|
+
profile: profileRuntime.profile,
|
|
203
226
|
});
|
|
204
227
|
const taskEvaluations = evaluateTaskContracts(taskSnapshot);
|
|
205
228
|
const executeEvaluations = taskEvaluations.filter((task) => task.phase !== 'verify');
|
|
@@ -251,6 +274,7 @@ export function runVerify(argv) {
|
|
|
251
274
|
}),
|
|
252
275
|
tasks: tasks.map((t) => ({ id: t.id, text: t.text, req: t.req || null, reqs: t.reqs || [], done: t.done })),
|
|
253
276
|
sensors: evidence,
|
|
277
|
+
tddAttestations,
|
|
254
278
|
};
|
|
255
279
|
writeAuthority('verificacao.json', `${JSON.stringify(pkg, null, 2)}\n`);
|
|
256
280
|
if (reqIds.length === 0) {
|