wendkeep 0.79.0 → 0.80.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 +26 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/commands/changes-and-verification.md +50 -0
- package/docs/en/commands/sessions-and-import.md +6 -0
- package/docs/en/commands/verify.md +9 -0
- package/docs/pt-BR/commands/changes-and-verification.md +50 -0
- package/docs/pt-BR/commands/sessions-and-import.md +7 -0
- package/docs/pt-BR/commands/verify.md +8 -0
- package/hooks/change-core.mjs +19 -1
- package/hooks/session-stop.mjs +40 -1
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +7 -0
- package/packages/vault/src/memory-handoff.mjs +15 -0
- package/schema/artifact-manifest-v1.schema.json +35 -0
- package/schema/handoff-contract-v1.schema.json +37 -0
- package/schema/task-contract-v1.schema.json +57 -0
- package/src/task-contracts.mjs +510 -0
- package/src/task-leases.mjs +105 -0
- package/src/task.mjs +115 -0
- package/src/verify.mjs +32 -0
package/src/task.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { activeChange } from '../hooks/change-core.mjs';
|
|
3
|
+
import { resolveActiveContext } from '../hooks/active-context-store.mjs';
|
|
4
|
+
import { resolveCommandActiveContext } from './active-context-runtime.mjs';
|
|
5
|
+
import { buildTaskContractSnapshot, evaluateTaskContracts } from './task-contracts.mjs';
|
|
6
|
+
import { claimTaskLease, releaseTaskLease } from './task-leases.mjs';
|
|
7
|
+
import { findProjectRoot } from '../packages/harness/src/sensors-core.mjs';
|
|
8
|
+
|
|
9
|
+
const HELP = `wendkeep task <list|show|evaluate|claim|release> [task-id]
|
|
10
|
+
|
|
11
|
+
--session <id> select the causal work session
|
|
12
|
+
--change <slug> select a change matching the active context
|
|
13
|
+
--lease-seconds <n> claim duration (1..86400; default 900)
|
|
14
|
+
--json emit structured JSON
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
function opt(argv, name) {
|
|
18
|
+
const index = argv.indexOf(name);
|
|
19
|
+
return index >= 0 ? argv[index + 1] : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fail(error, json = false) {
|
|
23
|
+
const payload = {
|
|
24
|
+
ok: false,
|
|
25
|
+
code: error?.code || 'TASK_COMMAND_FAILED',
|
|
26
|
+
error: String(error?.message || error),
|
|
27
|
+
recovery: error?.recovery || 'inspect the active context and task contract, then retry',
|
|
28
|
+
};
|
|
29
|
+
process.stderr.write(json ? `${JSON.stringify(payload)}\n` : `wendkeep task: ${payload.code}: ${payload.error}\n`);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function commandState(argv) {
|
|
34
|
+
const json = argv.includes('--json');
|
|
35
|
+
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
36
|
+
if (!vaultRaw) throw Object.assign(new Error('no vault (--vault or OBSIDIAN_VAULT_PATH)'), { code: 'TASK_VAULT_MISSING' });
|
|
37
|
+
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
38
|
+
const projectRoot = resolve(opt(argv, '--project') || findProjectRoot(process.cwd()) || process.cwd());
|
|
39
|
+
const sessionId = opt(argv, '--session') || process.env.CODEX_THREAD_ID || process.env.CLAUDE_SESSION_ID || '';
|
|
40
|
+
const identity = resolveCommandActiveContext({ vaultBase, projectRoot, sessionId, requireExisting: true });
|
|
41
|
+
if (!identity) throw Object.assign(new Error('active context is required'), { code: 'TASK_ACTIVE_CONTEXT_NOT_FOUND' });
|
|
42
|
+
const context = resolveActiveContext(vaultBase, identity);
|
|
43
|
+
const explicitChange = opt(argv, '--change');
|
|
44
|
+
const changeSlug = explicitChange || activeChange(vaultBase, { context: identity });
|
|
45
|
+
if (!changeSlug) throw Object.assign(new Error('no active change'), { code: 'TASK_CHANGE_NOT_FOUND' });
|
|
46
|
+
if (explicitChange && context.change_slug && explicitChange !== context.change_slug) {
|
|
47
|
+
throw Object.assign(new Error('requested change differs from active context'), { code: 'TASK_CHANGE_CONTEXT_MISMATCH' });
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
json, vaultBase, projectRoot, sessionId: identity.sessionId || sessionId,
|
|
51
|
+
identity, context, changeSlug,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function write(value, json, line = '') {
|
|
56
|
+
if (json) process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
57
|
+
else process.stdout.write(`${line || String(value)}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function runTask(argv = []) {
|
|
61
|
+
const sub = argv[0];
|
|
62
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
63
|
+
process.stdout.write(HELP);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
const json = argv.includes('--json');
|
|
67
|
+
try {
|
|
68
|
+
const state = commandState(argv);
|
|
69
|
+
const snapshot = buildTaskContractSnapshot(state);
|
|
70
|
+
const taskId = String(argv[1] || '').trim();
|
|
71
|
+
const contract = taskId ? snapshot.contracts.find((item) => item.task_id === taskId) : null;
|
|
72
|
+
if (sub !== 'list' && !contract) {
|
|
73
|
+
throw Object.assign(new Error(`task not found: ${taskId || '(missing id)'}`), { code: 'TASK_NOT_FOUND' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (sub === 'list') {
|
|
77
|
+
const value = { change_slug: state.changeSlug, tasks: snapshot.contracts };
|
|
78
|
+
write(value, state.json, snapshot.contracts.map((item) => `${item.task_id} [${item.status}] ${item.title}`).join('\n'));
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
if (sub === 'show') {
|
|
82
|
+
write(contract, state.json, `${contract.task_id} [${contract.status}] ${contract.title}`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
if (sub === 'evaluate') {
|
|
86
|
+
const evaluation = evaluateTaskContracts(snapshot).find((item) => item.task_id === taskId);
|
|
87
|
+
write(evaluation, state.json, `${taskId}: ${evaluation.can_complete ? 'can complete' : 'blocked'}${evaluation.blocking_findings.length ? ` — ${evaluation.blocking_findings.map((item) => item.code).join(', ')}` : ''}`);
|
|
88
|
+
return evaluation.can_complete ? 0 : 1;
|
|
89
|
+
}
|
|
90
|
+
if (sub === 'claim') {
|
|
91
|
+
const lease = claimTaskLease({
|
|
92
|
+
...state,
|
|
93
|
+
changeSlug: state.changeSlug,
|
|
94
|
+
taskId,
|
|
95
|
+
ownerSessionId: state.sessionId,
|
|
96
|
+
leaseSeconds: Number(opt(argv, '--lease-seconds') || 900),
|
|
97
|
+
});
|
|
98
|
+
write(lease, state.json, `task ${taskId} claimed by ${lease.owner_session_id} until ${lease.expires_at}`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (sub === 'release') {
|
|
102
|
+
const lease = releaseTaskLease({
|
|
103
|
+
...state,
|
|
104
|
+
changeSlug: state.changeSlug,
|
|
105
|
+
taskId,
|
|
106
|
+
ownerSessionId: state.sessionId,
|
|
107
|
+
});
|
|
108
|
+
write(lease, state.json, `task ${taskId} released`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
throw Object.assign(new Error(`unknown subcommand: ${sub}`), { code: 'TASK_SUBCOMMAND_UNKNOWN' });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return fail(error, json);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/verify.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parseTasks, activeChange, appendFixTasks, healSpecBacklinks } from '../hooks/change-core.mjs';
|
|
7
|
+
import { buildTaskContractSnapshot, evaluateTaskContracts } from './task-contracts.mjs';
|
|
7
8
|
import {
|
|
8
9
|
loadSensorsDetailed,
|
|
9
10
|
findProjectRoot,
|
|
@@ -189,6 +190,37 @@ export function runVerify(argv) {
|
|
|
189
190
|
}
|
|
190
191
|
if (!ok) { process.stderr.write(`verify: critical sensors red: ${failing.join(', ')}\n`); process.exit(1); }
|
|
191
192
|
|
|
193
|
+
// Execute -> Verify is a causal transition. Sensor execution above is allowed to capture the
|
|
194
|
+
// current envelope, but an active typed task contract must satisfy every authored gate before
|
|
195
|
+
// verify can announce success or assemble the deep package. Legacy projects without an active
|
|
196
|
+
// context preserve their pre-contract behavior until they migrate.
|
|
197
|
+
if (commandContext) {
|
|
198
|
+
const taskSnapshot = buildTaskContractSnapshot({
|
|
199
|
+
vaultBase,
|
|
200
|
+
projectRoot,
|
|
201
|
+
changeSlug: slug,
|
|
202
|
+
identity: commandContext,
|
|
203
|
+
});
|
|
204
|
+
const taskEvaluations = evaluateTaskContracts(taskSnapshot);
|
|
205
|
+
const executeEvaluations = taskEvaluations.filter((task) => task.phase !== 'verify');
|
|
206
|
+
const taskGate = {
|
|
207
|
+
schema_version: 1,
|
|
208
|
+
change_slug: slug,
|
|
209
|
+
evidence_envelope_id: envelope.envelope_id,
|
|
210
|
+
ok: executeEvaluations.every((task) => task.can_complete),
|
|
211
|
+
execute_task_ids: executeEvaluations.map((task) => task.task_id),
|
|
212
|
+
tasks: taskEvaluations,
|
|
213
|
+
};
|
|
214
|
+
writeAuthority('task-evaluation.json', `${JSON.stringify(taskGate, null, 2)}\n`);
|
|
215
|
+
if (!taskGate.ok) {
|
|
216
|
+
const blockers = [...new Set(executeEvaluations.flatMap((task) => (
|
|
217
|
+
task.blocking_findings.map((finding) => `${task.task_id}:${finding.code}`)
|
|
218
|
+
)))];
|
|
219
|
+
process.stderr.write(`verify: task contracts block Execute -> Verify: ${blockers.join(', ')}\n`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
192
224
|
// --deep (Q2=B): assemble the verification package the wk-verify skill judges. A trivial
|
|
193
225
|
// change (no [req:] tasks, sensors green) gets an auto verdict — no agent pass needed.
|
|
194
226
|
if (argv.includes('--deep')) {
|