@ngockhoale/ukit 3.4.45 → 3.4.47
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 +15 -6
- package/manifests/documentation.yaml +12 -0
- package/package.json +3 -1
- package/scripts/dev/capture-omp-stall.mjs +246 -0
- package/scripts/docs/render-instructions.mjs +29 -5
- package/src/core/agentRuntime/supervisor.js +15 -1
- package/src/core/executionContracts.js +21 -7
- package/src/diagnostics/ompStallSnapshot.js +199 -0
- package/template_project/.claude/hooks/skill-router.sh +23 -9
- package/template_project/.claude/ukit/index/route-resolver.mjs +21 -7
- package/template_project/.claude/ukit/runtime/cron-operation-adapter.mjs +1 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +56 -1
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -0
- package/template_project/.omp/hooks/pre/ukit-bridge.js +1 -1
- package/scripts/audit/decision-coverage.mjs +0 -322
- package/scripts/audit/subagent-cost/analyze.mjs +0 -107
- package/scripts/audit/subagent-cost/check-doc.mjs +0 -91
- package/scripts/audit/subagent-cost/duplicates.mjs +0 -195
- package/scripts/audit/subagent-cost/metrics.mjs +0 -193
- package/scripts/audit/subagent-cost/reader.mjs +0 -332
- package/scripts/audit/subagent-cost/render.mjs +0 -34
- package/scripts/audit/subagent-cost/usage-fields.mjs +0 -82
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Development-only incident capture: bounded metadata, no session control or payload persistence.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const ID_PATTERN = /^[\w.:/-]{1,120}$/;
|
|
7
|
+
const id = (value) => typeof value === 'string' && ID_PATTERN.test(value) ? value : null;
|
|
8
|
+
const time = (value) => Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : null;
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export async function runBoundedProcess(command, args = [], { timeoutMs = 650, maxBuffer = 65536 } = {}) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
let timedOut = false;
|
|
15
|
+
let oversized = false;
|
|
16
|
+
let stdoutBytes = 0;
|
|
17
|
+
let stderrBytes = 0;
|
|
18
|
+
const stdout = [];
|
|
19
|
+
const stderr = [];
|
|
20
|
+
let timer;
|
|
21
|
+
const finish = (result) => { if (!settled) { settled = true; clearTimeout(timer); resolve(result); } };
|
|
22
|
+
let child;
|
|
23
|
+
try { child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }); }
|
|
24
|
+
catch (error) { resolve({ ok: false, errorCode: error.code || 'UNKNOWN', stdout: '', stderr: '' }); return; }
|
|
25
|
+
timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, Math.max(1, timeoutMs));
|
|
26
|
+
const collect = (chunks, field) => (chunk) => {
|
|
27
|
+
const next = field === 'stdout' ? stdoutBytes + chunk.length : stderrBytes + chunk.length;
|
|
28
|
+
if (next + (field === 'stdout' ? stderrBytes : stdoutBytes) > maxBuffer) { oversized = true; child.kill('SIGKILL'); return; }
|
|
29
|
+
chunks.push(chunk);
|
|
30
|
+
if (field === 'stdout') stdoutBytes = next; else stderrBytes = next;
|
|
31
|
+
};
|
|
32
|
+
child.stdout.on('data', collect(stdout, 'stdout'));
|
|
33
|
+
child.stderr.on('data', collect(stderr, 'stderr'));
|
|
34
|
+
child.on('error', (error) => finish({ ok: false, errorCode: error.code || 'UNKNOWN', stdout: '', stderr: '' }));
|
|
35
|
+
child.on('close', (code) => finish({ ok: !timedOut && !oversized && code === 0,
|
|
36
|
+
errorCode: timedOut ? 'ETIMEDOUT' : oversized ? 'MAXBUFFER' : code === 0 ? null : 'EXIT_NONZERO',
|
|
37
|
+
exitCode: code, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') }));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readBoundedTail(file, { maxBytes = 262144 } = {}) {
|
|
42
|
+
const cap = Math.max(1, Math.min(1048576, maxBytes));
|
|
43
|
+
const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NONBLOCK || 0) | (constants.O_NOFOLLOW || 0));
|
|
44
|
+
try {
|
|
45
|
+
const stat = await handle.stat();
|
|
46
|
+
if (!stat.isFile()) throw new Error('capture requires a regular file');
|
|
47
|
+
const size = Math.min(cap, stat.size);
|
|
48
|
+
const buffer = Buffer.alloc(size);
|
|
49
|
+
const { bytesRead } = await handle.read(buffer, 0, size, Math.max(0, stat.size - size));
|
|
50
|
+
let text = buffer.subarray(0, bytesRead).toString('utf8');
|
|
51
|
+
const truncated = stat.size > size;
|
|
52
|
+
if (truncated) text = text.includes('\n') ? text.slice(text.indexOf('\n') + 1) : '';
|
|
53
|
+
return { text, bytesRead, truncated, size: stat.size, modifiedAt: stat.mtimeMs };
|
|
54
|
+
} finally { await handle.close(); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const operationKinds = ['model', 'tool', 'hook', 'subagent'];
|
|
58
|
+
const activeStates = new Set(['started', 'running', 'pending', 'waiting']);
|
|
59
|
+
const terminalStates = new Set(['completed', 'finished', 'failed', 'stopped', 'cancelled']);
|
|
60
|
+
export function processInventoryCommand(platform) {
|
|
61
|
+
return platform === 'win32' ? ['tasklist', ['/FO', 'CSV', '/NH']]
|
|
62
|
+
: ['ps', ['-A', '-o', 'pid=,ppid=,stat=,etime=,comm=']];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseProcessInventory(platform, text) {
|
|
66
|
+
if (platform === 'win32') return text.split(/\r?\n/).flatMap((line) => {
|
|
67
|
+
const fields = line.match(/"(?:[^"]|"")*"|[^,]+/g) || [];
|
|
68
|
+
const name = fields[0]?.replace(/^"|"$/g, '').replace(/""/g, '"');
|
|
69
|
+
const pid = Number(fields[1]?.replaceAll('"', ''));
|
|
70
|
+
return Number.isSafeInteger(pid) && pid > 0 ? [{ pid, name: (name || '').split(/[\\/]/).at(-1) }] : [];
|
|
71
|
+
});
|
|
72
|
+
return text.split('\n').flatMap((line) => {
|
|
73
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
|
|
74
|
+
return match ? [{ pid: Number(match[1]), name: match[5].trim().split(/[\\/]/).at(-1) }] : [];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseOwnerPids(command, text) {
|
|
79
|
+
const values = command === 'fuser' ? (text.match(/:\s*([^\r\n]+)/g) || []).flatMap((line) => line.slice(line.indexOf(':') + 1).trim().split(/\s+/).map((token) => token.match(/^(\d+)[A-Za-z]*$/)?.[1]).filter(Boolean))
|
|
80
|
+
: (text.match(/\b\d+\b/g) || []);
|
|
81
|
+
return [...new Set(values.map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0))];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function classifySessionActivity({ association = 'NOT_OBSERVABLE', summary = null, now = Date.now(), idleMs = 120000 } = {}) {
|
|
85
|
+
if (summary?.operatorBlock === 'OBSERVED_BLOCK') return 'OPERATOR_BLOCK';
|
|
86
|
+
if (summary?.sessionState === 'STOPPED') return 'STOPPED';
|
|
87
|
+
if (association !== 'ACTIVE_OWNER' || !summary) return 'NOT_OBSERVABLE';
|
|
88
|
+
const progressAge = Number.isFinite(summary.lastProgressAt) ? Math.max(0, now - summary.lastProgressAt) : null;
|
|
89
|
+
const outputAge = Number.isFinite(summary.lastOutputAt) ? Math.max(0, now - summary.lastOutputAt) : null;
|
|
90
|
+
const pending = summary.pendingOperations || {};
|
|
91
|
+
if (progressAge === null) return 'NOT_OBSERVABLE';
|
|
92
|
+
if (progressAge >= idleMs) return 'SUSPECTED_STALL';
|
|
93
|
+
if (Object.values(pending).some((items) => Array.isArray(items) && items.length > 0)
|
|
94
|
+
&& Object.entries(pending).some(([kind, items]) => items.length > 0 && summary.operationObservability?.[kind] === 'OBSERVED')) return 'WAITING';
|
|
95
|
+
if (outputAge !== null && outputAge >= idleMs) return 'NO_OUTPUT';
|
|
96
|
+
return 'ACTIVE_PROGRESS';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
export function summarizeSessionTail(text) {
|
|
101
|
+
const result = { sessionId: null, modelAlias: 'NOT_OBSERVABLE', resolvedRoute: 'NOT_OBSERVABLE', currentTurn: 'NOT_OBSERVABLE', lastCompletedTurn: 'NOT_OBSERVABLE',
|
|
102
|
+
pendingModelRequest: 'NOT_OBSERVABLE', lastChunk: 'NOT_OBSERVABLE', terminalEvent: 'NOT_OBSERVABLE', pendingTools: [], unmatchedToolCallCandidates: [], pendingSubagent: 'NOT_OBSERVABLE',
|
|
103
|
+
pendingOperations: { model: [], tool: [], hook: [], subagent: [] }, operationObservability: { model: 'NOT_OBSERVABLE', tool: 'NOT_OBSERVABLE', hook: 'NOT_OBSERVABLE', subagent: 'NOT_OBSERVABLE' },
|
|
104
|
+
operatorBlock: 'NOT_OBSERVABLE', turnState: 'NOT_OBSERVABLE', sessionState: 'NOT_OBSERVABLE', compaction: 'NOT_OBSERVABLE', stopContinuation: 'NOT_OBSERVABLE',
|
|
105
|
+
lastProgressAt: 'NOT_OBSERVABLE', lastOutputAt: 'NOT_OBSERVABLE', outputState: 'NOT_OBSERVABLE' };
|
|
106
|
+
const operations = Object.fromEntries(operationKinds.map((kind) => [kind, new Map()]));
|
|
107
|
+
const tools = new Map();
|
|
108
|
+
const observedOperations = new Set();
|
|
109
|
+
const operatorBlocks = new Map();
|
|
110
|
+
for (const line of text.split('\n')) {
|
|
111
|
+
let row;
|
|
112
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
113
|
+
const ts = time(row.timestamp);
|
|
114
|
+
if (ts) result.lastProgressAt = Date.parse(ts);
|
|
115
|
+
if (row.type === 'session' && id(row.id)) { result.sessionId = id(row.id); result.sessionIdentityObserved = true; }
|
|
116
|
+
if (row.type === 'compaction') result.compaction = { timestamp: ts, id: id(row.id), tokensBefore: Number.isFinite(row.tokensBefore) ? row.tokensBefore : null };
|
|
117
|
+
if (row.type === 'session_end' || row.type === 'session_stopped') result.sessionState = 'STOPPED';
|
|
118
|
+
|
|
119
|
+
if (row.type === 'operation' && operationKinds.includes(row.operation) && id(row.id)) {
|
|
120
|
+
observedOperations.add(row.operation);
|
|
121
|
+
const state = typeof row.state === 'string' ? row.state.toLowerCase() : '';
|
|
122
|
+
if (activeStates.has(state)) operations[row.operation].set(row.id, { id: row.id, ...(id(row.name) ? { name: id(row.name) } : {}) });
|
|
123
|
+
if (terminalStates.has(state)) operations[row.operation].delete(row.id);
|
|
124
|
+
}
|
|
125
|
+
const eventState = String(row.state || '').toLowerCase();
|
|
126
|
+
if (row.type === 'operator_block' || row.operation === 'operator') {
|
|
127
|
+
const key = id(row.id) || `block-${operatorBlocks.size}`;
|
|
128
|
+
if (['pending', 'blocked', 'waiting'].includes(eventState)) operatorBlocks.set(key, { id: id(row.id), state: 'BLOCKED' });
|
|
129
|
+
if (terminalStates.has(eventState) || ['unblocked', 'cleared'].includes(eventState)) operatorBlocks.delete(key);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const message = row.message;
|
|
133
|
+
if (!message) continue;
|
|
134
|
+
if (message.role === 'toolResult' && id(message.toolCallId)) tools.delete(message.toolCallId);
|
|
135
|
+
if (message.role !== 'assistant') continue;
|
|
136
|
+
result.modelAlias = { provider: id(message.provider), model: id(message.model) };
|
|
137
|
+
if (message.stopReason) {
|
|
138
|
+
const stopReason = ['stop', 'toolUse', 'error', 'aborted', 'length'].includes(message.stopReason) ? message.stopReason : 'other';
|
|
139
|
+
result.lastCompletedTurn = { id: id(row.id), timestamp: ts, stopReason };
|
|
140
|
+
result.terminalEvent = stopReason;
|
|
141
|
+
result.turnState = message.stopReason === 'aborted' ? 'TURN_ABORTED' : message.stopReason === 'error' ? 'TURN_ERROR' : 'TURN_COMPLETED';
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(message.content)) {
|
|
144
|
+
for (const part of message.content) {
|
|
145
|
+
if (part.type === 'toolCall' && id(part.id)) tools.set(part.id, { id: id(part.id), name: id(part.name) });
|
|
146
|
+
if (part.type === 'text' && ts) {
|
|
147
|
+
result.lastOutputAt = Date.parse(ts);
|
|
148
|
+
result.outputState = 'OBSERVED_OUTPUT';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
result.unmatchedToolCallCandidates = [...tools.values()].slice(-32);
|
|
154
|
+
result.pendingOperations = Object.fromEntries(operationKinds.map((kind) => [kind, [...operations[kind].values()].slice(-32)]));
|
|
155
|
+
result.pendingTools = result.pendingOperations.tool;
|
|
156
|
+
result.operationObservability = Object.fromEntries(operationKinds.map((kind) => [kind, observedOperations.has(kind) || (kind === 'tool' && tools.size > 0) ? 'OBSERVED' : 'NOT_OBSERVABLE']));
|
|
157
|
+
result.pendingModelRequest = result.operationObservability.model === 'OBSERVED' ? (result.pendingOperations.model.length ? 'OBSERVED_PENDING_EVENT' : 'NO_PENDING_EVENT_OBSERVED') : 'NOT_OBSERVABLE';
|
|
158
|
+
result.pendingSubagent = result.operationObservability.subagent === 'OBSERVED' ? (result.pendingOperations.subagent.length ? 'OBSERVED_PENDING_EVENT' : 'NO_PENDING_EVENT_OBSERVED') : 'NOT_OBSERVABLE';
|
|
159
|
+
result.operatorBlock = operatorBlocks.size ? 'OBSERVED_BLOCK' : 'NOT_OBSERVABLE';
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function associateSessionEvidence({ sessions = [], ownerLocks = {}, processInventory = { state: 'NOT_OBSERVABLE', processes: [] }, logs = [] } = {}) {
|
|
164
|
+
const sessionCounts = new Map();
|
|
165
|
+
for (const session of sessions) if (id(session.sessionId)) sessionCounts.set(session.sessionId, (sessionCounts.get(session.sessionId) || 0) + 1);
|
|
166
|
+
return sessions.map((session) => {
|
|
167
|
+
const sessionId = id(session.sessionId);
|
|
168
|
+
const lock = sessionId ? ownerLocks[sessionId] : null;
|
|
169
|
+
const lockState = lock?.state || 'NOT_OBSERVABLE';
|
|
170
|
+
const pids = lockState === 'OBSERVED' && Array.isArray(lock.pids) ? [...new Set(lock.pids.filter((pid) => Number.isSafeInteger(pid) && pid > 0))] : [];
|
|
171
|
+
const uniqueOwnerPid = pids.length === 1 ? pids[0] : null;
|
|
172
|
+
const duplicateSession = sessionId !== null && sessionCounts.get(sessionId) > 1;
|
|
173
|
+
const process = uniqueOwnerPid !== null && processInventory.state === 'OBSERVED'
|
|
174
|
+
? processInventory.processes.find((entry) => entry.pid === uniqueOwnerPid && /^omp(?:\.exe)?$/i.test(entry.name)) || null : null;
|
|
175
|
+
const processAssociation = duplicateSession ? 'AMBIGUOUS_TRANSCRIPT' : pids.length > 1 ? 'AMBIGUOUS_OWNER_LOCK'
|
|
176
|
+
: uniqueOwnerPid === null ? (lockState === 'NOT_OBSERVABLE' ? 'NOT_OBSERVABLE' : 'UNHELD')
|
|
177
|
+
: processInventory.state !== 'OBSERVED' ? 'NOT_OBSERVABLE' : process ? 'VERIFIED' : 'PID_NOT_FOUND';
|
|
178
|
+
const ownerPid = duplicateSession ? null : uniqueOwnerPid;
|
|
179
|
+
const identityVerified = session?.sessionIdentityObserved === true || session?.sourceCoverage?.sessionIdentityObserved === true;
|
|
180
|
+
const ompLog = ownerPid === null ? null : logs.find((entry) => entry.pid === ownerPid) || null;
|
|
181
|
+
const logAssociation = ownerPid === null ? 'NOT_OBSERVABLE' : ompLog ? 'VERIFIED' : 'NOT_OBSERVABLE';
|
|
182
|
+
return { sessionId, transcript: typeof session.transcriptPath === 'string' ? session.transcriptPath.split(/[\\/]/).at(-1) : null,
|
|
183
|
+
sessionAssociation: identityVerified && processAssociation === 'VERIFIED' && logAssociation === 'VERIFIED' ? 'ACTIVE_OWNER' : 'NOT_OBSERVABLE',
|
|
184
|
+
ownerLockState: duplicateSession ? 'AMBIGUOUS_TRANSCRIPT' : lockState, ownerPid, processAssociation, logAssociation, identityVerified,
|
|
185
|
+
process: process ? { pid: process.pid, name: id(process.name) } : null, ompLog };
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildStallSnapshot({ now = Date.now(), lastProgressAt = null, lastOutputAt = null, idleMs = 120000, executionState = 'NOT_OBSERVABLE', ...evidence } = {}) {
|
|
190
|
+
const age = (ts) => Number.isFinite(ts) ? Math.max(0, now - ts) : null;
|
|
191
|
+
const progressAgeMs = age(lastProgressAt);
|
|
192
|
+
const outputAgeMs = age(lastOutputAt);
|
|
193
|
+
const activity = evidence.activity || (executionState === 'NO_EXECUTION' ? 'NO_EXECUTION'
|
|
194
|
+
: executionState !== 'EXECUTING' ? 'NOT_OBSERVABLE'
|
|
195
|
+
: progressAgeMs === null ? 'NOT_OBSERVABLE' : progressAgeMs >= idleMs ? 'NO_OBSERVED_PROGRESS'
|
|
196
|
+
: outputAgeMs !== null && outputAgeMs >= idleMs ? 'NO_OUTPUT' : 'RECENT_PROGRESS');
|
|
197
|
+
return { schemaVersion: 1, capturedAt: new Date(now).toISOString(), verdict: 'INSUFFICIENT_EVIDENCE', activity,
|
|
198
|
+
executionState, progressAgeMs, outputAgeMs, ...evidence };
|
|
199
|
+
}
|
|
@@ -1818,25 +1818,39 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
1818
1818
|
// FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
|
|
1819
1819
|
// the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
|
|
1820
1820
|
// sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
|
|
1821
|
-
// Vietnamese negators
|
|
1821
|
+
// Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
1822
1822
|
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
1823
1823
|
const NON_ORDER_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|restart|sua|viet|chinh|them|tao|xoa';
|
|
1824
1824
|
function neutralizeNonOrders(text) {
|
|
1825
1825
|
return String(text)
|
|
1826
1826
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1827
|
+
.replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1828
|
+
.replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
|
|
1829
|
+
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
1830
|
+
.replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
|
|
1831
|
+
.replace(/\bchinh\s+xac\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b/g, ' ')
|
|
1827
1832
|
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1828
1833
|
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1829
1834
|
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
1830
1835
|
}
|
|
1831
1836
|
|
|
1832
1837
|
function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1833
|
-
|
|
1838
|
+
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
1839
|
+
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
1840
|
+
const preflight = releaseText.match(/\b(?:yarn\s+release:verify|npm\s+run\s+release:verify)\b/)?.[0];
|
|
1841
|
+
const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
|
|
1842
|
+
&& /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
|
|
1843
|
+
? { kind: 'conditional-release', preflight } : null;
|
|
1844
|
+
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
|
|
1834
1845
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
1835
1846
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
1836
1847
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
1837
1848
|
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1838
1849
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
1839
1850
|
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
1851
|
+
const reportArtifact = String(promptText).match(/(?:^|\s)((?:docs\/|reports\/)[A-Za-z0-9_./-]+\.(?:md|txt))(?=\s|[.,;:]|$)/)?.[1];
|
|
1852
|
+
const documentation = !sourceOrder && !/\b(?:implement|fix|refactor|modify|edit|update|sua|chinh)\b/.test(actionable.join(' ')) && reportArtifact && actionable.some((clause) => /\b(?:write|create|save|viet|tao|luu)\b/.test(clause))
|
|
1853
|
+
&& /\b(?:report|bao cao|analysis|phan tich|document)\b/.test(text);
|
|
1840
1854
|
const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
|
|
1841
1855
|
// A cron/job MENTION is not an operation: with a code-change verb in the request, an
|
|
1842
1856
|
// operation needs a concrete target id; otherwise it is a coding task about scheduling.
|
|
@@ -1861,13 +1875,13 @@ function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
|
1861
1875
|
// lookup verb AND no mutation vocabulary anywhere in the request (fail-closed otherwise).
|
|
1862
1876
|
const mutationWord = /\b(?:implement|fix|refactor|modify|edit|write|update|add|create|build|make|install|deploy|remove|delete|rename|replace|change|sua|viet|chinh|them|tao|xoa|doi|cai|cap nhat)\b/.test(text);
|
|
1863
1877
|
const statusReport = !mutationWord && !operation && !config
|
|
1864
|
-
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua)\b/.test(text);
|
|
1865
|
-
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(text) || statusReport)
|
|
1878
|
+
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text);
|
|
1879
|
+
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|debug|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text) || statusReport)
|
|
1866
1880
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
1867
1881
|
const taskKind = operation ? (sourceOrder || (nativeDomain !== 'scheduled-job' && codeChangeVerb) ? 'MIXED' : 'OPERATIONAL')
|
|
1868
|
-
: config ? 'CONFIGURATION' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
1882
|
+
: config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
1869
1883
|
const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
|
|
1870
|
-
return { taskKind, ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
1884
|
+
return { taskKind, ...(releasePolicy ? { releasePolicy } : {}), ...(documentation ? { reportArtifact } : {}), ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
1871
1885
|
...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
|
|
1872
1886
|
...(operation && nativeDomain === 'scheduled-job' ? { operationGoal: { domain: 'scheduled-job', target: id, desiredState: /\b(?:delete|remove|cancel|xoa|huy|absent|removed|deleted|no longer|khong con)\b/.test(actionable.join(' ')) ? 'absent' : 'present',
|
|
1873
1887
|
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text) ? 'ensure-state' : 'execute-and-verify' } } : {}) };
|
|
@@ -1878,7 +1892,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
1878
1892
|
const operationOnly = semantic.taskKind === 'OPERATIONAL'
|
|
1879
1893
|
|| (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
|
|
1880
1894
|
return { ...(contract || {}), ...semantic,
|
|
1881
|
-
completionEvidence: operationOnly ? ['operation-verification']
|
|
1895
|
+
completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
|
|
1882
1896
|
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
1883
1897
|
: semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
|
|
1884
1898
|
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|
|
@@ -2238,7 +2252,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2238
2252
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
2239
2253
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
2240
2254
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
2241
|
-
.replace(new RegExp(`\\
|
|
2255
|
+
.replace(new RegExp(`\\b(?:khong|chua)\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
2242
2256
|
.replace(new RegExp(`\\bdung\\s+${TAIL}`, 'g'), ' ')
|
|
2243
2257
|
// C89-008: a waiting clause narrates what the TURN waits on, not what
|
|
2244
2258
|
// the request orders — verbs inside it ("chờ sửa xong", "waiting for
|
|
@@ -2312,7 +2326,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
2312
2326
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|not|khong(?:\\s+duoc)?|tuyet\\s+doi\\s+khong)\\s+(?:${MUT_VERBS})(?![A-Za-z0-9_])(?:\\s*(?:,|,?\\s+(?:or|and|nor|hay|hoac|va))\\s*(?:(?:${MUT_VERBS})(?![A-Za-z0-9_])|(?:${MUT_VERBS})\\s))*[^.,;\\n\u2014]*?(?=\\s+(?:va|and|then|roi|nhung|but)\\s|[.,;\u2014\\n]|\\s*$)`, 'g'), ' ')
|
|
2313
2327
|
.replace(new RegExp(`\\bgiu\\s+nguyen\\b${TAIL}`, 'g'), ' ')
|
|
2314
2328
|
.replace(new RegExp(`\\bmuon\\s+giu\\b${TAIL}`, 'g'), ' ')
|
|
2315
|
-
.replace(new RegExp(`\\
|
|
2329
|
+
.replace(new RegExp(`\\b(?:khong|chua)\\s+(?!chi\\b)${TAIL}`, 'g'), ' ')
|
|
2316
2330
|
.replace(new RegExp(`\\bdung\\s+${TAIL}`, 'g'), ' ')
|
|
2317
2331
|
// C89-008: a waiting clause narrates what the TURN waits on, not what
|
|
2318
2332
|
// the request orders — verbs inside it belong to the wait.
|
|
@@ -1090,25 +1090,39 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
1090
1090
|
// FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
|
|
1091
1091
|
// the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
|
|
1092
1092
|
// sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
|
|
1093
|
-
// Vietnamese negators
|
|
1093
|
+
// Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
1094
1094
|
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
1095
1095
|
const NON_ORDER_VERBS = 'implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|edit|restart|sua|viet|chinh|them|tao|xoa';
|
|
1096
1096
|
function neutralizeNonOrders(text) {
|
|
1097
1097
|
return String(text)
|
|
1098
1098
|
.replace(new RegExp(`\\b(?:do\\s+not|don't|dont|must\\s+not|never|shall\\s+not)\\s+[^.;\\n]*?(?=(?<!,)\\s+(?:and|va)\\s+(?:${NON_ORDER_VERBS})\\b|\\s+(?:but|nhung|then|however)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1099
|
+
.replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
1100
|
+
.replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
|
|
1101
|
+
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
1102
|
+
.replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
|
|
1103
|
+
.replace(/\bchinh\s+xac\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b/g, ' ')
|
|
1099
1104
|
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
1100
1105
|
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
1101
1106
|
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
1102
1107
|
}
|
|
1103
1108
|
|
|
1104
1109
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
1105
|
-
|
|
1110
|
+
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
1111
|
+
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
1112
|
+
const preflight = releaseText.match(/\b(?:yarn\s+release:verify|npm\s+run\s+release:verify)\b/)?.[0];
|
|
1113
|
+
const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
|
|
1114
|
+
&& /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
|
|
1115
|
+
? { kind: 'conditional-release', preflight } : null;
|
|
1116
|
+
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
|
|
1106
1117
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
1107
1118
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
1108
1119
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
1109
1120
|
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
1110
1121
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
1111
1122
|
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
1123
|
+
const reportArtifact = String(promptText).match(/(?:^|\s)((?:docs\/|reports\/)[A-Za-z0-9_./-]+\.(?:md|txt))(?=\s|[.,;:]|$)/)?.[1];
|
|
1124
|
+
const documentation = !sourceOrder && !/\b(?:implement|fix|refactor|modify|edit|update|sua|chinh)\b/.test(actionable.join(' ')) && reportArtifact && actionable.some((clause) => /\b(?:write|create|save|viet|tao|luu)\b/.test(clause))
|
|
1125
|
+
&& /\b(?:report|bao cao|analysis|phan tich|document)\b/.test(text);
|
|
1112
1126
|
const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
|
|
1113
1127
|
// A cron/job MENTION is not an operation: with a code-change verb in the request, an
|
|
1114
1128
|
// operation needs a concrete target id; otherwise it is a coding task about scheduling.
|
|
@@ -1133,13 +1147,13 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1133
1147
|
// lookup verb AND no mutation vocabulary anywhere in the request (fail-closed otherwise).
|
|
1134
1148
|
const mutationWord = /\b(?:implement|fix|refactor|modify|edit|write|update|add|create|build|make|install|deploy|remove|delete|rename|replace|change|sua|viet|chinh|them|tao|xoa|doi|cai|cap nhat)\b/.test(text);
|
|
1135
1149
|
const statusReport = !mutationWord && !operation && !config
|
|
1136
|
-
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua)\b/.test(text);
|
|
1137
|
-
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(text) || statusReport)
|
|
1150
|
+
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text);
|
|
1151
|
+
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|investigate|debug|forensic|root cause|kiem tra|phan tich|dieu tra|doc log|tim hieu)\b/.test(text) || statusReport)
|
|
1138
1152
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
1139
1153
|
const taskKind = operation ? (sourceOrder || codeChangeVerb ? 'MIXED' : 'OPERATIONAL')
|
|
1140
|
-
: config ? 'CONFIGURATION' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
1154
|
+
: config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
1141
1155
|
const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
|
|
1142
|
-
return { taskKind, ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
1156
|
+
return { taskKind, ...(releasePolicy ? { releasePolicy } : {}), ...(documentation ? { reportArtifact } : {}), ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
1143
1157
|
...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
|
|
1144
1158
|
...(operation && nativeDomain === 'scheduled-job' ? { operationGoal: { domain: 'scheduled-job', target: id, desiredState: /\b(?:delete|remove|cancel|xoa|huy|absent|removed|deleted|no longer|khong con)\b/.test(actionable.join(' ')) ? 'absent' : 'present',
|
|
1145
1159
|
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text)
|
|
@@ -1153,7 +1167,7 @@ export function applyTaskSemantics(contract, semantic) {
|
|
|
1153
1167
|
const operationOnly = semantic.taskKind === 'OPERATIONAL'
|
|
1154
1168
|
|| (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
|
|
1155
1169
|
return { ...(contract || {}), ...semantic,
|
|
1156
|
-
completionEvidence: operationOnly ? ['operation-verification']
|
|
1170
|
+
completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
|
|
1157
1171
|
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
1158
1172
|
: semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
|
|
1159
1173
|
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|
|
@@ -140,7 +140,7 @@ export function bashOperationReceipt(rawCommand, response, transportSuccess, goa
|
|
|
140
140
|
// the registry actually printed a semver. Agent prose is never evidence; the completion gate compares the
|
|
141
141
|
// recorded version with package.json and live Git state before it treats a release as published.
|
|
142
142
|
const NPM_PUBLISH = /^npm\s+publish(?:\s|$)/;
|
|
143
|
-
const NPM_VIEW = /^npm\s+view\s+(\S+?)(?:@\S+)?\s+version(?:\s+2>&1)?\s*$/;
|
|
143
|
+
const NPM_VIEW = /^npm\s+view\s+(\S+?)(?:@\S+)?\s+version(?:\s+--prefer-online)?(?:\s+2>&1)?\s*$/;
|
|
144
144
|
const SEMVER_LINE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
145
145
|
export function releaseReceipt(rawCommand, response, transportSuccess) {
|
|
146
146
|
if (typeof rawCommand !== 'string') return null;
|
|
@@ -2225,6 +2225,8 @@ function applyReceiptCore(ledger, receipt, { vibecode = false } = {}) {
|
|
|
2225
2225
|
if (receipt.kind === 'operation') {
|
|
2226
2226
|
next.operationBank = foldOperationEvidence(ledger.operationBank, receipt, ledger);
|
|
2227
2227
|
}
|
|
2228
|
+
if (receipt.releasePreflight) next.releasePreflight = { ...receipt.releasePreflight, sequence: next.receiptSequence };
|
|
2229
|
+
if (receipt.kind === 'write' && receipt.success) next.releasePreflight = null;
|
|
2228
2230
|
if (receipt.kind === 'source') {
|
|
2229
2231
|
next.sourceSucceeded = next.sourceSucceeded || receipt.success;
|
|
2230
2232
|
if (receipt.success && receipt.file) {
|
|
@@ -2848,6 +2850,12 @@ export async function recordExecutionReceipt({
|
|
|
2848
2850
|
exitCode,
|
|
2849
2851
|
};
|
|
2850
2852
|
|
|
2853
|
+
const policy = routeState?.routeSummary?.executionContract?.releasePolicy;
|
|
2854
|
+
if (toolName === 'Bash' && !bashScope.foreign && policy?.kind === 'conditional-release'
|
|
2855
|
+
&& String(toolInput.command || '').trim() === policy.preflight && Number.isInteger(exitCode)) {
|
|
2856
|
+
receipt.releasePreflight = { command: policy.preflight, failed: exitCode !== 0, exitCode,
|
|
2857
|
+
taskInstanceId: routeState.taskInstanceId, workspaceFp };
|
|
2858
|
+
}
|
|
2851
2859
|
const toolResponse = payload.tool_response ?? payload.tool_output ?? payload.tool_result;
|
|
2852
2860
|
const nativeGoal = routeState?.routeSummary?.executionContract?.operationGoal;
|
|
2853
2861
|
const operationReceipt = cronOperationReceipt(toolName, toolInput, toolResponse, success, { sessionId: explicitSessionId(payload), harness })
|
|
@@ -2863,6 +2871,14 @@ export async function recordExecutionReceipt({
|
|
|
2863
2871
|
} else if (toolName === 'Edit' || toolName === 'Write') {
|
|
2864
2872
|
receipt.kind = 'write';
|
|
2865
2873
|
receipt.file = toolInput.file_path || toolInput.path || toolInput.paths?.[0] || null;
|
|
2874
|
+
// External scratch artifacts are not product mutations. Keep them out of write debt/proof entirely;
|
|
2875
|
+
// otherwise a /tmp report resurrects implementation obligations on a read-only investigation.
|
|
2876
|
+
if (typeof receipt.file === 'string') {
|
|
2877
|
+
const relative = path.relative(path.resolve(projectRoot), path.resolve(projectRoot, receipt.file));
|
|
2878
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2879
|
+
return { rejected: true, eventId: null, reason: 'outside-product-workspace' };
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2866
2882
|
} else if (toolName === 'Bash' && !bashScope.foreign && (isVerificationCommand(toolInput.command) || evidence)) {
|
|
2867
2883
|
// TASK-009 (BL-011): a Bash command carrying an evidence attestation
|
|
2868
2884
|
// records as a verification receipt even when the command is not a known
|
|
@@ -3004,6 +3020,14 @@ function requiredEvidence(state = {}) {
|
|
|
3004
3020
|
|
|
3005
3021
|
function evidenceSatisfied(evidence, ledger = {}, state = {}, { cwd } = {}) {
|
|
3006
3022
|
const routeSummary = state?.routeSummary || {};
|
|
3023
|
+
if (evidence === 'report-artifact') {
|
|
3024
|
+
const artifact = routeSummary.executionContract?.reportArtifact;
|
|
3025
|
+
if (!cwd || typeof artifact !== 'string') return false;
|
|
3026
|
+
const target = path.resolve(cwd, artifact);
|
|
3027
|
+
const bank = evidenceBank(ledger, 'bankedWrites');
|
|
3028
|
+
if (bank[evidenceBankKey(artifact)] !== true && bank[evidenceBankKey(target)] !== true) return false;
|
|
3029
|
+
try { return fsSync.statSync(target).isFile() && fsSync.statSync(target).size > 0; } catch { return false; }
|
|
3030
|
+
}
|
|
3007
3031
|
if (evidence === 'operation-verification') {
|
|
3008
3032
|
return operationCompletionPolicy(scopedContract(state, ledger), null, withGitAuthority(ledger, cwd))?.complete === true;
|
|
3009
3033
|
}
|
|
@@ -3513,6 +3537,23 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3513
3537
|
const at = missingEvidence.indexOf('impact-evidence');
|
|
3514
3538
|
if (at !== -1) missingEvidence.splice(at, 1);
|
|
3515
3539
|
}
|
|
3540
|
+
const releasePolicy = routeSummary.executionContract?.releasePolicy;
|
|
3541
|
+
const preflight = effectiveLedger?.releasePreflight;
|
|
3542
|
+
if (['MIXED', 'OPERATIONAL'].includes(routeSummary.executionContract?.taskKind)
|
|
3543
|
+
&& releasePolicy?.kind === 'conditional-release' && preflight?.failed === true
|
|
3544
|
+
&& preflight.command === releasePolicy.preflight && preflight.taskInstanceId === state.taskInstanceId
|
|
3545
|
+
&& (!preflight.workspaceFp || preflight.workspaceFp === workspaceFingerprint(cwd))
|
|
3546
|
+
&& !missingEvidence.some((item) => item !== 'operation-verification' && item !== 'verification-evidence')) {
|
|
3547
|
+
return { continue: false, notify: true, complete: false, outcome: 'blocked',
|
|
3548
|
+
releaseOutcome: 'RELEASE_NO_GO', missingEvidence: [],
|
|
3549
|
+
reason: `RELEASE_NO_GO: owned preflight ${preflight.command} failed (exit ${preflight.exitCode}); publication is prohibited by this task's conditional release policy. No release was completed.` };
|
|
3550
|
+
}
|
|
3551
|
+
if (releasePolicy?.kind === 'conditional-release'
|
|
3552
|
+
&& !(preflight?.failed === false && preflight.command === releasePolicy.preflight
|
|
3553
|
+
&& preflight.taskInstanceId === state.taskInstanceId
|
|
3554
|
+
&& (!preflight.workspaceFp || preflight.workspaceFp === workspaceFingerprint(cwd)))) {
|
|
3555
|
+
missingEvidence.push('release-preflight');
|
|
3556
|
+
}
|
|
3516
3557
|
const releaseMissing = releasePublicationMissing(state, effectiveLedger, cwd);
|
|
3517
3558
|
if (releaseMissing) missingEvidence.push(releaseMissing);
|
|
3518
3559
|
// TASK-009 (BL-011): the bug-fix floor. On a `playbookId: bug-fix` route a
|
|
@@ -3731,7 +3772,7 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3731
3772
|
|
|
3732
3773
|
const finalAttempt = !vibecode && continuationCount === MAX_CONTINUATIONS - 1;
|
|
3733
3774
|
const instruction = missingEvidence.includes('release-publication')
|
|
3734
|
-
? 'The release is not proven published: run `npm publish` once if the registry does not yet serve this version, then confirm with `npm view <package> version` (must equal package.json) and that the release tag dereferences to HEAD (or to an ancestor of HEAD with no shipped change since). Never move the tag. Do not republish a version the registry already serves.'
|
|
3775
|
+
? 'The release is not proven published: run `npm publish` once if the registry does not yet serve this version, then confirm with `npm view <package> version` (must equal package.json) and that the release tag dereferences to HEAD (or to an ancestor of HEAD with no shipped change since). Never move the tag. Do not republish a version the registry already serves. Accepted evidence: a Bash result of exactly `npm view <package> version` (optionally `2>&1`, optionally after `cd <dir> &&`) that exited 0 and printed a bare semver. Rejected as unproven: the same command behind `| tail/head/cat`, `;`, `||` or inside a compound (a pipeline hides the exit status), dry runs, prose claims. Re-run the bare command once; do not republish.'
|
|
3735
3776
|
: operationalTask
|
|
3736
3777
|
? 'Inspect native mutation receipts and verify the requested operational state; do not fabricate source edits or source-test evidence.'
|
|
3737
3778
|
: recoveryInstruction(missingEvidence, effectiveLedger, routeSummary, { cwd });
|
|
@@ -3770,6 +3811,20 @@ function playbookStopAdvisory({ state = {}, ledger = {} } = {}, result = {}) {
|
|
|
3770
3811
|
|
|
3771
3812
|
export function evaluateCompletion(args = {}) {
|
|
3772
3813
|
const result = evaluateCompletionContract(args);
|
|
3814
|
+
// Explain the verdict from the SAME task-scoped snapshot used by the verifier. These are proof summaries,
|
|
3815
|
+
// not an alternative evidence source; a diagnostic must never satisfy or relax an obligation.
|
|
3816
|
+
const owned = effectiveRequestLedger(args.state, args.ledger) || {};
|
|
3817
|
+
const diagnostics = {
|
|
3818
|
+
taskInstanceId: args.state?.taskInstanceId || null,
|
|
3819
|
+
contract: args.state?.routeSummary?.executionContract?.taskKind || args.state?.routeSummary?.taskKind || args.state?.routeSummary?.executionMode || null,
|
|
3820
|
+
unmetObligations: result.missingEvidence || [],
|
|
3821
|
+
acceptedEvidence: result.complete === true || result.missingEvidence?.length
|
|
3822
|
+
? requiredEvidence(args.state).filter((item) => !(result.missingEvidence || []).includes(item)) : [],
|
|
3823
|
+
observedEvidence: [owned.sourceSucceeded && 'source-read', owned.writeSucceeded && 'write-receipt', owned.verificationSucceeded && 'verification-receipt'].filter(Boolean),
|
|
3824
|
+
rejectedEvidence: args.ledger && owned !== args.ledger ? ['ledger-task-ownership-mismatch'] : [],
|
|
3825
|
+
};
|
|
3826
|
+
result.diagnostics = diagnostics;
|
|
3827
|
+
if (result.reason) result.reason += ` [task=${diagnostics.taskInstanceId || 'unknown'} contract=${diagnostics.contract || 'unknown'} accepted-count=${diagnostics.acceptedEvidence.length} rejected=${diagnostics.rejectedEvidence.join(',') || 'none'}]`;
|
|
3773
3828
|
try {
|
|
3774
3829
|
const advisory = playbookStopAdvisory(args, result);
|
|
3775
3830
|
if (advisory) return { ...result, playbookAdvisory: advisory };
|
|
@@ -1966,6 +1966,7 @@ export async function runStopCoordinator({
|
|
|
1966
1966
|
await captureUsageShadow({ projectRoot, payload });
|
|
1967
1967
|
} catch { /* advisory */ }
|
|
1968
1968
|
|
|
1969
|
+
|
|
1969
1970
|
// TASK-005 (FR-005): the coordinator's terminal verdict lands ONE
|
|
1970
1971
|
// `outcome.observed` record per coordinated stop — the merged evaluator
|
|
1971
1972
|
// decision is the session's honest outcome at this stop. Fire-and-forget:
|
|
@@ -1176,7 +1176,7 @@ export async function runSessionStop(
|
|
|
1176
1176
|
// Command authority (RC-A): same shared phase signal the Claude Stop path uses, so a
|
|
1177
1177
|
// handoff-create/review session is never pushed toward Edit/Write on omp either.
|
|
1178
1178
|
const phase = await resolveStopPhase(payload.transcript_path);
|
|
1179
|
-
evaluation = evaluateCompletion({ state, ledger, phase });
|
|
1179
|
+
evaluation = evaluateCompletion({ state, ledger, phase, cwd: projectRoot });
|
|
1180
1180
|
} catch (error) {
|
|
1181
1181
|
// BUG-C22-05: fail-closed like the shell gate (continue the turn), but only up
|
|
1182
1182
|
// to the configured consecutive-crash cap — a persistently broken evaluator
|