@deniscodp/skills 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +89 -2
- package/bin/deniscodp-skills.mjs +4 -0
- package/package.json +23 -4
- package/scripts/catalog.mjs +68 -0
- package/scripts/cli.mjs +264 -0
- package/scripts/install.mjs +184 -0
- package/scripts/recommended.mjs +102 -0
- package/skills/denisco-handoff-implementation/SKILL.md +254 -0
- package/skills/denisco-handoff-implementation/references/blocker-checklist.md +71 -0
- package/skills/denisco-handoff-implementation/references/delegated-implementation.md +168 -0
- package/skills/denisco-handoff-implementation/references/ownership.md +105 -0
- package/skills/denisco-handoff-implementation/references/session-plan.md +195 -0
- package/skills/denisco-handoff-implementation/scripts/ownership.mjs +153 -0
- package/skills/denisco-implementation-coordinator/SKILL.md +237 -0
- package/skills/denisco-implementation-coordinator/agents/openai.yaml +2 -0
- package/skills/denisco-implementation-coordinator/references/documents.md +175 -0
- package/skills/denisco-implementation-coordinator/references/runtimes.md +105 -0
- package/skills/denisco-interactive-spec-definition/SKILL.md +126 -0
- package/skills/denisco-interactive-spec-definition/references/blocker-checklist.md +71 -0
- package/skills/denisco-interactive-spec-definition/references/handoff-template.md +124 -0
- package/skills/denisco-project-context/SKILL.md +60 -0
- package/skills/denisco-project-context/references/project-documents.md +70 -0
- package/skills/denisco-visual-check/SKILL.md +136 -0
- package/skills/denisco-visual-check/references/judge-prompt.md +55 -0
- package/skills/denisco-visual-check/references/project-config.md +118 -0
- package/skills/denisco-workflow-benchmark/SKILL.md +113 -0
- package/skills/denisco-workflow-benchmark/agents/openai.yaml +2 -0
- package/skills/denisco-workflow-benchmark/references/measurement.md +166 -0
- package/skills/denisco-workflow-benchmark/references/workflow-contract.md +371 -0
- package/skills/denisco-workflow-benchmark/scripts/bench.mjs +156 -0
- package/skills/denisco-workflow-benchmark/scripts/cases.mjs +46 -0
- package/skills/denisco-workflow-benchmark/scripts/configuration.mjs +71 -0
- package/skills/denisco-workflow-benchmark/scripts/discover.mjs +88 -0
- package/skills/denisco-workflow-benchmark/scripts/eval-worker.mjs +58 -0
- package/skills/denisco-workflow-benchmark/scripts/io.mjs +82 -0
- package/skills/denisco-workflow-benchmark/scripts/prepare.mjs +69 -0
- package/skills/denisco-workflow-benchmark/scripts/runs.mjs +235 -0
- package/skills/denisco-workflow-benchmark/scripts/state-prompt.mjs +96 -0
- package/skills/denisco-workflow-benchmark/scripts/tracking.mjs +91 -0
- package/skills/denisco-workflow-benchmark/scripts/usage.mjs +175 -0
- package/skills/denisco-workflow-benchmark/scripts/workflow.mjs +334 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { hash, inside, loadRun, readJSON, rejectSymlinkPath, safeId, writeNew, writeJSON } from './io.mjs';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { collect, summarize, usageState } from './runs.mjs';
|
|
7
|
+
import { checkStatePrompt, updateStatePrompt } from './state-prompt.mjs';
|
|
8
|
+
|
|
9
|
+
const canonical = file => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
|
|
10
|
+
const samePath = (a, b) => canonical(a) === canonical(b);
|
|
11
|
+
export const workflowPath = handoff => `${path.resolve(handoff)}.workflow.json`;
|
|
12
|
+
const contractHash = run => hash(JSON.stringify(run.criteria.map(c => ({ id: c.id, expectation: c.expectation, required: c.required !== false })).sort((a, b) => a.id.localeCompare(b.id))));
|
|
13
|
+
function realFile(file) {
|
|
14
|
+
rejectSymlinkPath(file);
|
|
15
|
+
if (!fs.statSync(file).isFile()) throw new Error(`Expected a file: ${file}`);
|
|
16
|
+
return file;
|
|
17
|
+
}
|
|
18
|
+
function ids(values, label, allowEmpty = false) {
|
|
19
|
+
if (!Array.isArray(values) || (!allowEmpty && !values.length) || new Set(values).size !== values.length) throw new Error(`Invalid ${label}: expected unique IDs`);
|
|
20
|
+
values.forEach(safeId);
|
|
21
|
+
}
|
|
22
|
+
function validate(data, handoff, { documents = false } = {}) {
|
|
23
|
+
if (data.schemaVersion !== 1) throw new Error('Unsupported workflow format');
|
|
24
|
+
safeId(data.taskId);
|
|
25
|
+
const base = path.dirname(workflowPath(handoff)), paths = {};
|
|
26
|
+
for (const key of ['handoff', 'plan', 'state']) {
|
|
27
|
+
if (typeof data[key] !== 'string' || !data[key].trim()) throw new Error(`Missing workflow ${key}`);
|
|
28
|
+
paths[key] = path.resolve(base, data[key]);
|
|
29
|
+
if (documents || key === 'handoff') realFile(paths[key]);
|
|
30
|
+
}
|
|
31
|
+
if (!samePath(paths.handoff, handoff)) throw new Error('Workflow refers to a different handoff');
|
|
32
|
+
if (new Set(Object.values(paths).map(canonical)).size !== 3) throw new Error('Handoff, plan and state must be distinct files');
|
|
33
|
+
if (!['enabled', 'disabled', 'unknown'].includes(data.measurement?.status)) throw new Error('Invalid measurement status');
|
|
34
|
+
if (data.measurement.status !== 'enabled' && Object.keys(data.measurement).some(k => k !== 'status')) throw new Error('Inactive measurement must not retain an ambiguous run binding');
|
|
35
|
+
if (!Array.isArray(data.blocks)) throw new Error('Missing block map');
|
|
36
|
+
ids(data.blocks.map(b => b.id), 'blocks', true);
|
|
37
|
+
for (const block of data.blocks) {
|
|
38
|
+
ids(block.points, `${block.id} points`);
|
|
39
|
+
ids(block.criteria, `${block.id} criteria`);
|
|
40
|
+
ids(block.dependsOn, `${block.id} dependencies`, true);
|
|
41
|
+
if (block.dependsOn.some(id => !data.blocks.some(b => b.id === id))) throw new Error(`Unknown dependency in ${block.id}`);
|
|
42
|
+
}
|
|
43
|
+
const visiting = new Set(), visited = new Set();
|
|
44
|
+
function visit(id) {
|
|
45
|
+
if (visiting.has(id)) throw new Error('Cyclic block dependencies');
|
|
46
|
+
if (visited.has(id)) return;
|
|
47
|
+
visiting.add(id);
|
|
48
|
+
data.blocks.find(b => b.id === id).dependsOn.forEach(visit);
|
|
49
|
+
visiting.delete(id); visited.add(id);
|
|
50
|
+
}
|
|
51
|
+
data.blocks.forEach(b => visit(b.id));
|
|
52
|
+
if (data.visualChecks !== undefined) {
|
|
53
|
+
if (!Array.isArray(data.visualChecks) || new Set(data.visualChecks.map(check => check.id)).size !== data.visualChecks.length) throw new Error('Invalid visual check map');
|
|
54
|
+
for (const check of data.visualChecks) {
|
|
55
|
+
safeId(check.id); safeId(check.block); ids(check.criteria, `${check.id} criteria`);
|
|
56
|
+
if (!data.blocks.some(block => block.id === check.block)) throw new Error(`Unknown visual check block: ${check.block}`);
|
|
57
|
+
if (check.criteria.some(criterion => !data.blocks.find(block => block.id === check.block)?.criteria.includes(criterion))) throw new Error(`Visual check criterion is outside its block: ${check.id}`);
|
|
58
|
+
if (!['principal', 'subagent'].includes(check.executor)) throw new Error(`Invalid visual check executor: ${check.id}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return paths;
|
|
62
|
+
}
|
|
63
|
+
function checkRun(data, run, paths, { complete = false } = {}) {
|
|
64
|
+
// A task can have a project ID distinct from the benchmark ID; the explicit binding carries both.
|
|
65
|
+
inside(run.project, path.relative(run.project, paths.handoff));
|
|
66
|
+
const assigned = new Set(data.blocks.flatMap(b => b.criteria));
|
|
67
|
+
for (const id of assigned) if (!run.criteria.some(c => c.id === id)) throw new Error(`Unknown mapped criterion: ${id}`);
|
|
68
|
+
if (complete) {
|
|
69
|
+
if (!data.blocks.length) throw new Error('Block map is not ready');
|
|
70
|
+
const missing = run.criteria.filter(c => c.required !== false && !assigned.has(c.id));
|
|
71
|
+
if (missing.length) throw new Error(`Required criteria omitted from block map: ${missing.map(c => c.id).join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function bind({ handoff, run: file, task, plan, state }) {
|
|
76
|
+
handoff = realFile(path.resolve(handoff));
|
|
77
|
+
file = realFile(path.resolve(file));
|
|
78
|
+
const run = loadRun(file), target = workflowPath(handoff), base = path.dirname(target);
|
|
79
|
+
rejectSymlinkPath(target);
|
|
80
|
+
const exists = fs.existsSync(target);
|
|
81
|
+
const data = exists ? readJSON(target) : {
|
|
82
|
+
schemaVersion: 1, taskId: task ?? run.task, handoff: path.basename(handoff),
|
|
83
|
+
plan: path.relative(base, plan ? path.resolve(plan) : path.join(base, 'PLAN.md')),
|
|
84
|
+
state: path.relative(base, state ? path.resolve(state) : path.join(base, 'STATE.md')),
|
|
85
|
+
measurement: { status: 'unknown' }, blocks: []
|
|
86
|
+
};
|
|
87
|
+
const original = JSON.stringify(data);
|
|
88
|
+
const paths = validate(data, handoff);
|
|
89
|
+
if ((task && task !== data.taskId) || (plan && !samePath(plan, paths.plan)) || (state && !samePath(state, paths.state))) throw new Error('Existing workflow paths/task conflict; review without overwriting');
|
|
90
|
+
const measurement = { status: 'enabled', run: file, runId: run.id, taskId: run.task, criteriaHash: contractHash(run) };
|
|
91
|
+
if (data.measurement.status === 'disabled') throw new Error('Measurement explicitly disabled; review the workflow before binding');
|
|
92
|
+
if (data.measurement.status === 'enabled' && !sameBinding(data.measurement, measurement)) throw new Error('Conflicting run binding or changed criteria; no files changed');
|
|
93
|
+
checkRun(data, run, paths);
|
|
94
|
+
const assigned = new Set(data.blocks.flatMap(b => b.criteria));
|
|
95
|
+
const mapReady = data.blocks.length > 0 && run.criteria.filter(c => c.required !== false).every(c => assigned.has(c.id));
|
|
96
|
+
if (data.measurement.status === 'enabled') return { workflow: target, changed: false, mapReady };
|
|
97
|
+
data.measurement = measurement;
|
|
98
|
+
// The sidecar is the only write target. Existing documents and run history are not rewritten.
|
|
99
|
+
if (exists) {
|
|
100
|
+
const lock = `${target}.lock`, fd = fs.openSync(lock, 'wx');
|
|
101
|
+
try {
|
|
102
|
+
if (JSON.stringify(readJSON(target)) !== original) throw new Error('Workflow changed during bind; retry after review');
|
|
103
|
+
// writeJSON is atomic; use it only after rechecking the existing sidecar under its lock.
|
|
104
|
+
writeJSON(target, data);
|
|
105
|
+
} finally { fs.closeSync(fd); fs.unlinkSync(lock); }
|
|
106
|
+
} else writeNew(target, `${JSON.stringify(data, null, 2)}\n`);
|
|
107
|
+
return { workflow: target, changed: true, mapReady };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sameBinding(a, b) {
|
|
111
|
+
return typeof a.run === 'string' && path.isAbsolute(a.run) && samePath(a.run, b.run)
|
|
112
|
+
&& a.runId === b.runId && a.taskId === b.taskId && a.criteriaHash === b.criteriaHash;
|
|
113
|
+
}
|
|
114
|
+
export function readWorkflow(handoff, { draft = false } = {}) {
|
|
115
|
+
const file = realFile(workflowPath(handoff)), data = readJSON(file);
|
|
116
|
+
const paths = validate(data, path.resolve(handoff), { documents: !draft });
|
|
117
|
+
let run = null;
|
|
118
|
+
if (data.measurement.status === 'enabled') {
|
|
119
|
+
const binding = data.measurement;
|
|
120
|
+
if (typeof binding.run !== 'string' || !path.isAbsolute(binding.run)) throw new Error('Measurement run must be an absolute path');
|
|
121
|
+
run = loadRun(realFile(binding.run));
|
|
122
|
+
if (!sameBinding(binding, { run: binding.run, runId: run.id, taskId: run.task, criteriaHash: contractHash(run) })) throw new Error('Run identity or criteria changed since binding');
|
|
123
|
+
checkRun(data, run, paths, { complete: !draft });
|
|
124
|
+
}
|
|
125
|
+
return { file, data, paths, run };
|
|
126
|
+
}
|
|
127
|
+
function blockQuality(run, block) {
|
|
128
|
+
if (!run) return null;
|
|
129
|
+
const criteria = run.criteria.filter(c => block.criteria.includes(c.id));
|
|
130
|
+
if (!criteria.some(c => c.required !== false)) return { quality: 'unverified', passed: 0, required: 0 };
|
|
131
|
+
return summarize({ ...run, criteria });
|
|
132
|
+
}
|
|
133
|
+
export function checkpoint({ handoff, block: id, implementation, sessionKey, sessionState = 'unknown', claim, nextBlock }) {
|
|
134
|
+
if (!['complete', 'in-progress', 'not-started'].includes(implementation)) throw new Error('Declare implementation: complete|in-progress|not-started');
|
|
135
|
+
if (!['active', 'closed', 'unknown'].includes(sessionState)) throw new Error('Invalid session state');
|
|
136
|
+
if (claim && !['planned', 'in-progress', 'implemented-unverified', 'verified'].includes(claim)) throw new Error('Invalid claimed point status');
|
|
137
|
+
const { file, data, paths, run } = readWorkflow(handoff);
|
|
138
|
+
const block = data.blocks.find(b => b.id === id);
|
|
139
|
+
if (!block) throw new Error(`Unknown block: ${id}`);
|
|
140
|
+
const result = blockQuality(run, block);
|
|
141
|
+
const requiredVisual = (data.visualChecks ?? []).filter(check => check.block === id && check.required !== false);
|
|
142
|
+
const visualResults = requiredVisual.map(required => {
|
|
143
|
+
const attempts = (run?.visualChecks ?? []).filter(check => check.id === required.id);
|
|
144
|
+
const latest = attempts.at(-1);
|
|
145
|
+
const session = latest?.sessionKey && run.sessions.find(session => session.key === latest.sessionKey);
|
|
146
|
+
const executor = latest?.executor ?? 'subagent';
|
|
147
|
+
const attributed = session && (executor === 'subagent'
|
|
148
|
+
? session.parent && (!latest.principalSession || session.parent === latest.principalSession)
|
|
149
|
+
: !session.parent && !session.agentId && (!latest.principalSession || session.key === latest.principalSession));
|
|
150
|
+
const valid = Boolean(executor === required.executor && latest?.block === required.block
|
|
151
|
+
&& latest?.status === 'finished' && latest.skillLoaded === true && attributed
|
|
152
|
+
&& required.criteria.every(criterion => latest.results.some(r => r.criterion === criterion && r.status === 'pass')));
|
|
153
|
+
return { id: required.id, executor: required.executor, status: valid ? 'verified' : 'unverified', attempt: latest?.id ?? null,
|
|
154
|
+
durationMs: latest?.finishedAt ? Date.parse(latest.finishedAt) - Date.parse(latest.startedAt) : null };
|
|
155
|
+
});
|
|
156
|
+
const visualReady = visualResults.every(check => check.status === 'verified');
|
|
157
|
+
const status = implementation === 'not-started' ? 'planned' : implementation === 'in-progress' ? 'in-progress'
|
|
158
|
+
: result?.quality === 'verified' && visualReady ? 'verified' : 'implemented-unverified';
|
|
159
|
+
const criteria = block.criteria.map(id => {
|
|
160
|
+
const e = run?.evidence.filter(e => e.criterion === id).at(-1);
|
|
161
|
+
return { id, recordedStatus: e?.status ?? null, evidence: e?.path ?? null,
|
|
162
|
+
verification: run ? blockQuality(run, { criteria: [id] }).quality : 'not-checked' };
|
|
163
|
+
});
|
|
164
|
+
const warnings = [];
|
|
165
|
+
let registration = 'not-applicable';
|
|
166
|
+
if (run) {
|
|
167
|
+
const session = run.sessions.find(s => s.key === sessionKey);
|
|
168
|
+
registration = !sessionKey ? 'identity-pending' : !session ? 'not-registered' : 'registered';
|
|
169
|
+
if (!session) warnings.push('Identify and register this session explicitly; do not select the latest project log automatically.');
|
|
170
|
+
if (session) {
|
|
171
|
+
if (!session.coverageConfirmed) warnings.push('Session scope and subagent coverage are not confirmed.');
|
|
172
|
+
if (!session.configurationId) warnings.push('Session skill configuration is not attributed. Capture and associate it at entry; do not infer historical configuration from current files.');
|
|
173
|
+
warnings.push(...session.metrics.warnings);
|
|
174
|
+
if (!fs.existsSync(session.log) || hash(fs.readFileSync(session.log)) !== session.metrics.sourceHash) warnings.push('Session log changed or is unavailable: collect is required before final accounting.');
|
|
175
|
+
}
|
|
176
|
+
if (sessionState !== 'closed') warnings.push('Usage is provisional; collect again after the session closes.');
|
|
177
|
+
} else warnings.push(`Measurement ${data.measurement.status}: only structure and links checked, not acceptance evidence.`);
|
|
178
|
+
for (const visual of visualResults.filter(check => check.status !== 'verified')) warnings.push(`${visual.id}: ${visual.executor} denisco-visual-check evidence is missing, incomplete or not loaded by the registered executor.`);
|
|
179
|
+
if (claim && claim !== status) warnings.push(`Claim ${claim} contradicts computed status ${status}.`);
|
|
180
|
+
const continuation = nextBlock === undefined ? null : nextPrompt({ handoff, block: nextBlock, check: true });
|
|
181
|
+
if (continuation && !continuation.valid) warnings.push(`Continuation prompt ${continuation.status}: save the assigned prompt in STATE before handoff.`);
|
|
182
|
+
return { workflow: file, block: id, paths, implementation, status, verification: result?.quality ?? 'not-checked',
|
|
183
|
+
passed: result?.passed ?? null, required: result?.required ?? null, criteria,
|
|
184
|
+
measurement: { status: data.measurement.status, run: data.measurement.run ?? null, registration, sessionState },
|
|
185
|
+
claimMatches: !claim || claim === status, warnings,
|
|
186
|
+
...(continuation ? { continuation } : {}), visualChecks: visualResults,
|
|
187
|
+
dependencies: block.dependsOn.map(id => ({ block: id, verification: blockQuality(run, data.blocks.find(b => b.id === id))?.quality ?? 'not-checked' })),
|
|
188
|
+
limitation: 'Evidence integrity is not semantic proof. Dependency readiness and Markdown consistency require review; this command neither reads claims from prose nor proves implementation completeness.' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const benchPath = () => fileURLToPath(new URL('./bench.mjs', import.meta.url));
|
|
192
|
+
|
|
193
|
+
// Prompts stay short: ownership, evidence reconciliation and authorization rules live in
|
|
194
|
+
// denisco-handoff-implementation, and task-specific constraints live in STATE.
|
|
195
|
+
|
|
196
|
+
// Specification can deliver this before PLAN, STATE or an planned block exists.
|
|
197
|
+
export function planningPrompt({ handoff, save = false, check = false }) {
|
|
198
|
+
if (save && check) throw new Error('Choose only one of --save or --check');
|
|
199
|
+
const context = readWorkflow(handoff, { draft: true });
|
|
200
|
+
const { data, paths } = context;
|
|
201
|
+
const fingerprint = value => hash(JSON.stringify({ data: value.data, paths: value.paths }));
|
|
202
|
+
const contextHash = fingerprint(context);
|
|
203
|
+
const measurement = data.measurement.status === 'enabled'
|
|
204
|
+
? `Measurement is explicitly enabled and bound to ${data.measurement.run}. Use denisco-workflow-benchmark to register this conversation, preserve the binding and collect/report at closure. Keep identifiers internal.`
|
|
205
|
+
: data.measurement.status === 'disabled'
|
|
206
|
+
? 'Measurement is disabled. Preserve measurement.status as disabled in every later session. Do not initialize, discover or invoke measurement tooling or registers.'
|
|
207
|
+
: 'Measurement status is unknown. Treat measurement as inactive: do not initialize, discover or invoke measurement tooling or registers unless a current explicit activation request is recorded.';
|
|
208
|
+
const prompt = `Use denisco-handoff-implementation with ${paths.handoff}.\nPhase: planning only. Read the handoff and operational context ${context.file}; read ${paths.plan} and ${paths.state} if present, preserving existing decisions and progress.\nCheck the handoff's blocker table and permission list; ask in chat about any unresolved item before choosing blocks.\nDeliver PLAN, STATE, the complete block map and a saved, checked first execution prompt. Do not implement application code in this phase.\nOnce they are saved, choose the execution path as denisco-handoff-implementation specifies: prepare coordination and ask its start question, or ask the user "Plan ready. Start <first block>?". Only the user's yes to the start question starts implementation; native plan approval, automatic mode or generic transition text do not.\n${measurement}\nIn Plan Mode propose the documents and first execution prompt in chat and name pending writes. Link the saved STATE prompt in the final response.\n`;
|
|
209
|
+
const args = { state: paths.handoff, block: 'planning', prompt, contextHash };
|
|
210
|
+
const result = save ? updateStatePrompt({ ...args, validateContext: () => {
|
|
211
|
+
if (fingerprint(readWorkflow(handoff, { draft: true })) !== contextHash) throw new Error('Workflow changed during planning prompt update');
|
|
212
|
+
} }) : check ? checkStatePrompt(args) : { status: 'printed', prompt };
|
|
213
|
+
const { state, ...output } = result;
|
|
214
|
+
return { ...output, handoff: paths.handoff, stage: 'planning' };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// One command at block entry: refresh registered usage, then show
|
|
218
|
+
// the register and the assignment. Its only write is the reimport of already registered logs.
|
|
219
|
+
export function open({ handoff, block: id, phase, nonce, provider }) {
|
|
220
|
+
const { data, paths, run } = readWorkflow(handoff);
|
|
221
|
+
const block = data.blocks.find(b => b.id === id);
|
|
222
|
+
if (!block) throw new Error(`Unknown planned block: ${id}`);
|
|
223
|
+
if (provider !== undefined && !['codex', 'claude'].includes(provider)) throw new Error('Invalid provider');
|
|
224
|
+
if (phase !== undefined && !['exploration', 'specification', 'planning', 'implementation', 'verification', 'correction'].includes(phase)) throw new Error('Invalid phase');
|
|
225
|
+
const warnings = [];
|
|
226
|
+
let accounting = { collected: false, sessions: [] }, register = null;
|
|
227
|
+
if (run) {
|
|
228
|
+
const before = new Map(run.sessions.map(s => [s.key, s.metrics.usage.total]));
|
|
229
|
+
// Refresh file snapshots; this does not establish that another conversation ended.
|
|
230
|
+
const refreshed = collect(data.measurement.run);
|
|
231
|
+
accounting = { collected: true, sessions: refreshed.sessions.map(s => ({ key: s.key, phase: s.phase,
|
|
232
|
+
state: usageState(s), total: s.metrics.usage.total,
|
|
233
|
+
delta: s.metrics.usage.total === null || before.get(s.key) == null ? null : s.metrics.usage.total - before.get(s.key) })) };
|
|
234
|
+
const summary = summarize(refreshed);
|
|
235
|
+
register = { quality: summary.quality, passed: summary.passed, required: summary.required,
|
|
236
|
+
review: summary.review.status, sessions: summary.sessions,
|
|
237
|
+
criteria: refreshed.criteria.map(c => ({ id: c.id, block: data.blocks.find(b => b.criteria.includes(c.id))?.id ?? null,
|
|
238
|
+
status: refreshed.evidence.filter(e => e.criterion === c.id).at(-1)?.status ?? 'not-run' })) };
|
|
239
|
+
warnings.push(...summary.warnings);
|
|
240
|
+
} else warnings.push(`Measurement ${data.measurement.status}: no usage is collected and acceptance cannot be verified automatically.`);
|
|
241
|
+
const continuation = nextPrompt({ handoff, block: id, check: true });
|
|
242
|
+
if (!continuation.valid) warnings.push(`Assigned prompt is ${continuation.status}: confirm this block is the authorized assignment before working.`);
|
|
243
|
+
const marker = nonce ?? `wfbench-${crypto.randomUUID()}`;
|
|
244
|
+
const identity = run ? {
|
|
245
|
+
marker,
|
|
246
|
+
whoami: `node ${benchPath()} whoami --provider ${provider ?? '<codex|claude>'} --project ${run.project} --nonce ${marker}`,
|
|
247
|
+
session: `node ${benchPath()} session --run ${data.measurement.run} --provider ${provider ?? '<codex|claude>'} --id <NATIVE_ID> --log <LOG> --phase ${phase ?? '<PHASE>'}`,
|
|
248
|
+
note: 'Emit the marker from a tool call, then resolve it with whoami; if no log carries it yet, repeat in a later call. Never select a log by recency.'
|
|
249
|
+
} : null;
|
|
250
|
+
return { block: id, paths, points: block.points, criteria: block.criteria, dependsOn: block.dependsOn,
|
|
251
|
+
measurement: { status: data.measurement.status, run: data.measurement.run ?? null },
|
|
252
|
+
accounting, register, continuation, identity, warnings,
|
|
253
|
+
limitation: 'Collect refreshes only registered logs. A session nobody registered stays invisible, and coverage remains an operator attestation.' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// One command at closure: persist the assigned continuation, check the block, refresh usage.
|
|
257
|
+
// It attests nothing: coverage, acceptance and the claimed status stay with the operator.
|
|
258
|
+
export function close({ handoff, block: id, implementation, nextBlock, final = false, sessionKey, sessionState, claim, ticket }) {
|
|
259
|
+
if (Boolean(nextBlock) === Boolean(final)) throw new Error('Declare exactly one continuation: --next-block ID or --final');
|
|
260
|
+
const applied = [];
|
|
261
|
+
try {
|
|
262
|
+
if (ticket) {
|
|
263
|
+
if (sessionKey) throw new Error('Choose --ticket or --session-key');
|
|
264
|
+
realFile(ticket);
|
|
265
|
+
const request = readJSON(ticket), context = readWorkflow(handoff);
|
|
266
|
+
if (request.schemaVersion !== 1 || !request.handoff || !request.run || !context.run
|
|
267
|
+
|| !samePath(request.handoff, context.paths.handoff) || !samePath(request.run, context.data.measurement.run)
|
|
268
|
+
|| request.runId !== context.run.id) throw new Error('Registration ticket binding does not match this handoff');
|
|
269
|
+
const attempt = context.run.registrationAttempts?.find(a => samePath(a.ticket, ticket));
|
|
270
|
+
if (attempt?.status !== 'registered' || !context.run.sessions.some(s => s.key === attempt.sessionKey)) throw new Error('Registration ticket is not registered; resolve tracking before using it to close');
|
|
271
|
+
sessionKey = attempt.sessionKey;
|
|
272
|
+
sessionState ??= 'active';
|
|
273
|
+
}
|
|
274
|
+
const continuation = final ? nextPrompt({ handoff, clear: true }) : nextPrompt({ handoff, block: nextBlock, save: true });
|
|
275
|
+
applied.push(final ? 'next-prompt --clear' : `next-prompt --save --block ${nextBlock}`);
|
|
276
|
+
const result = checkpoint({ handoff, block: id, implementation, sessionKey, sessionState, claim,
|
|
277
|
+
...(final ? {} : { nextBlock }) });
|
|
278
|
+
applied.push('checkpoint');
|
|
279
|
+
const { data, run } = readWorkflow(handoff);
|
|
280
|
+
const runFile = data.measurement.run ?? null;
|
|
281
|
+
let accounting = { collected: false, sessions: [] };
|
|
282
|
+
const remaining = [];
|
|
283
|
+
if (run) {
|
|
284
|
+
// Usage stays provisional while this conversation is active; the next read refreshes it.
|
|
285
|
+
const refreshed = collect(runFile);
|
|
286
|
+
applied.push('collect');
|
|
287
|
+
accounting = { collected: true, sessions: refreshed.sessions.map(s => ({ key: s.key, phase: s.phase, state: usageState(s), total: s.metrics.usage.total })) };
|
|
288
|
+
remaining.push(`Agent/observer: refresh again after this conversation closes with collect --run ${runFile}; do not infer closure from another conversation starting.`);
|
|
289
|
+
remaining.push(`Agent: regenerate the report with report --run ${runFile}; retain pending trailing usage while this conversation is active.`);
|
|
290
|
+
if (refreshed.sessions.some(s => !s.coverageConfirmed)) remaining.push('Coverage is unconfirmed: review scope and subagents, then collect --coverage-confirmed. Never pass it automatically.');
|
|
291
|
+
if (refreshed.review.status !== 'accepted') remaining.push('User acceptance is still pending: record it with review when the user gives it.');
|
|
292
|
+
}
|
|
293
|
+
return { ...result, applied, continuation, accounting, remaining };
|
|
294
|
+
} catch (error) {
|
|
295
|
+
error.message = `${error.message} (applied before failing: ${applied.join(', ') || 'nothing'})`;
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function nextPrompt({ handoff, block: id, save = false, check = false, clear = false }) {
|
|
301
|
+
if ([save, check, clear].filter(Boolean).length > 1) throw new Error('Choose only one of --save, --check or --clear');
|
|
302
|
+
if (clear && id !== undefined) throw new Error('--clear does not accept --block');
|
|
303
|
+
const context = readWorkflow(handoff);
|
|
304
|
+
const { data, paths } = context;
|
|
305
|
+
const fingerprint = ({ data, paths }) => hash(JSON.stringify({ data, paths }));
|
|
306
|
+
const contextHash = fingerprint(context);
|
|
307
|
+
const validateContext = () => {
|
|
308
|
+
if (fingerprint(readWorkflow(handoff)) !== contextHash) throw new Error('Workflow changed during prompt update; retry after review');
|
|
309
|
+
};
|
|
310
|
+
if (clear) return updateStatePrompt({ state: paths.state, clear: true, validateContext });
|
|
311
|
+
const block = data.blocks.find(b => b.id === id);
|
|
312
|
+
if (!block) throw new Error(`Unknown planned block: ${id}`);
|
|
313
|
+
const measurement = data.measurement.status === 'enabled'
|
|
314
|
+
? `Measurement run: ${data.measurement.run}\nBenchmark task: ${data.measurement.taskId}; project task: ${data.taskId}.\nUse denisco-workflow-benchmark track with this handoff, actual provider and phase; execute its returned nextCall yourself in a separate tool call. Keep tickets and identifiers internal, never pass them to the next conversation or ask the user to fill them. Register attributable children with evidenced parent links.\nRegistration/accounting from previous sessions may still be pending: inspect the register, do not assume completion.\nRecord evidence after final validation. Usage is provisional until collect after session closure. Never confirm coverage automatically.\nUse open with this handoff and block ${id} to refresh registered usage and inspect the assignment.\nWhen this session stops, close with denisco-workflow-benchmark close for this handoff and the last block worked, the actual implementation state, and the proposed next block or same-block resume (or --final when no task work remains, or the user ends the task or asks for no prompt). It saves the continuation, checkpoints and refreshes usage. The agent also generates report; trailing live usage and user acceptance remain pending where applicable.`
|
|
315
|
+
: data.measurement.status === 'disabled'
|
|
316
|
+
? 'Measurement is disabled. Preserve measurement.status as disabled in every later session. Do not initialize, discover or invoke measurement tooling or registers. Record verification evidence in STATE.md.'
|
|
317
|
+
: 'Measurement status is unknown and remains inactive. Do not initialize, discover or invoke measurement tooling or registers without a current explicit activation request. Record verification evidence in STATE.md.';
|
|
318
|
+
const visual = (data.visualChecks ?? []).filter(check => check.block === id && check.required !== false);
|
|
319
|
+
const principal = visual.filter(check => check.executor === 'principal');
|
|
320
|
+
const delegated = visual.filter(check => check.executor === 'subagent');
|
|
321
|
+
const describe = checks => checks.map(check => `${check.id} (${check.criteria.join(', ')})`).join('; ');
|
|
322
|
+
const visualInstruction = visual.length ? `Visual checks: ${principal.length ? `execute in the principal session: ${describe(principal)}.` : ''} ${delegated.length ? `Delegated assignments in the map: ${describe(delegated)}. Verify that the current user request explicitly delegates these checks; an old map alone is not permission to launch a child. Reconcile future assignments with the user request without relabeling historical attempts. For a requested delegation, give each child an exclusive attempt directory and prohibit source, task-state, register and shared-tooling writes.` : 'Do not delegate the whole visual check unless the user requests it.'} Reconcile existing evidence before repeating a check. Load denisco-visual-check explicitly, start each attempt with visual-start --executor principal|subagent as assigned, and finish with visual-finish using the registered executor session and actual results/evidence. For perceptual criteria only, use a fresh blind judge when available and permitted. Register its attributable native log separately; it does not replace the denisco-visual-check executor. Keep measured source and capture tooling stable during capture. Close resources created solely for the check, preserving pre-existing services and resources explicitly needed for subsequent work. The principal merges shared records serially.` : '';
|
|
323
|
+
const prompt = [
|
|
324
|
+
`Use denisco-handoff-implementation to continue task ${data.taskId} (${paths.handoff}) from block ${id}.`,
|
|
325
|
+
`Read ${paths.state} first: Your actions, pending blockers and the next step are there. Plan: ${paths.plan}. Workflow context: ${workflowPath(handoff)}.`,
|
|
326
|
+
`Block ${id}: points ${block.points.join(', ')}; criteria ${block.criteria.join(', ')}; depends on ${block.dependsOn.join(', ') || 'no other block'}.`,
|
|
327
|
+
measurement,
|
|
328
|
+
visualInstruction
|
|
329
|
+
].filter(Boolean).join('\n') + '\n';
|
|
330
|
+
const options = { state: paths.state, block: id, prompt, contextHash };
|
|
331
|
+
if (check) return checkStatePrompt(options);
|
|
332
|
+
if (save) return updateStatePrompt({ ...options, validateContext });
|
|
333
|
+
return prompt;
|
|
334
|
+
}
|