@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
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
5
|
+
## 3.4.47 - 2026-10-18
|
|
6
|
+
|
|
7
|
+
**Supervisor subagent usage attribution and rendered-instruction parity checks.**
|
|
8
|
+
- Record subagent usage at the supervisor's terminal completion/cancellation boundary, using typed operation metadata and existing exactly-once guards. Provider-authenticated usage remains blocked; host transcript values are not relabeled as provider data.
|
|
9
|
+
- Add read-only `render-instructions.mjs --parity` to byte-compare rendered root instructions with existing root files, including fixture-target support and drift diagnostics.
|
|
10
|
+
- Add manual-only `/ukit:ci-check` cross-platform CI tooling; it remains opt-in and never pushes, merges, or publishes.
|
|
11
|
+
- Locally verified on macOS/Node 22. Q-C98-1 and Q-C98-3 remain blocked on provider-authenticated data/privacy approval; no provider identity/pricing/cache model is claimed.
|
|
12
|
+
## 3.4.46 - 2026-10-18
|
|
13
|
+
|
|
14
|
+
**Completion contract P0: forensic intent, attributable release evidence and conditional NO-GO.**
|
|
15
|
+
- Correct negated/quoted forensic requests and Vietnamese status questions; explicit report artifacts require owned writes and nonempty output. External scratch writes do not count as product changes.
|
|
16
|
+
- Conditional release contracts with a named `release:verify` preflight accept only current-task attributable native failure as terminal `RELEASE_NO_GO`, never publication success. Missing implementation writes still block; passing preflight retains publication obligations. Masked commands and stale receipts remain unproven.
|
|
17
|
+
- Accept standalone `npm view <package> version --prefer-online` evidence without republishing; improve task/obligation diagnostics. OMP artifact checks use the installed project cwd.
|
|
18
|
+
- Exclude only repository coverage/transcript audit scripts from npm; retain runtime and customer assets and the strict artifact budget.
|
|
19
|
+
- Verified locally on macOS/Node 22 with real isolated OMP child-hook chains; no provider calls or GitHub Actions dispatch. Historical release ledger replay remains partial; OMP mid-task freeze remains open. UNIC Decision stays unchanged/off.
|
|
11
20
|
|
|
12
21
|
## 3.4.45 - 2026-10-17
|
|
13
22
|
|
|
@@ -955,6 +955,18 @@ entries:
|
|
|
955
955
|
archive_policy: never
|
|
956
956
|
notes: C69 G7 operator doc — timeline render, support bundle contents/exclusions, replay limits
|
|
957
957
|
|
|
958
|
+
- id: docs-completion-decision-assessment
|
|
959
|
+
path: docs/COMPLETION_DECISION_ASSESSMENT.md
|
|
960
|
+
class: canonical
|
|
961
|
+
audience: [maintainer]
|
|
962
|
+
owner: runtime
|
|
963
|
+
source_of_truth: docs/COMPLETION_DECISION_ASSESSMENT.md
|
|
964
|
+
merge_strategy: none
|
|
965
|
+
load_policy: on-demand
|
|
966
|
+
validation: [manual]
|
|
967
|
+
archive_policy: never
|
|
968
|
+
notes: P0 completion contract corrections and offline semantic decision architecture assessment
|
|
969
|
+
|
|
958
970
|
- id: docs-unic-decision-integration-p1-fix
|
|
959
971
|
path: docs/UNIC-DECISION-INTEGRATION-P1-FIX.md
|
|
960
972
|
class: canonical
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ngockhoale/ukit",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.47",
|
|
4
4
|
"description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex and omp (Oh My Pi).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"!scripts/audit/c100/implementer-attribution.mjs",
|
|
35
35
|
"!scripts/audit/c100/trees-lifetimes.mjs",
|
|
36
36
|
"!scripts/audit/c100/usage-provenance.mjs",
|
|
37
|
+
"!scripts/audit/decision-coverage.mjs",
|
|
38
|
+
"!scripts/audit/subagent-cost/**",
|
|
37
39
|
"template_project/",
|
|
38
40
|
"template_user/",
|
|
39
41
|
"!template_project/**/.smbdelete*",
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One-shot development capture. Every subprocess is bounded; OMP is never signalled or modified.
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { readBoundedTail, summarizeSessionTail, buildStallSnapshot, associateSessionEvidence, classifySessionActivity, parseProcessInventory, processInventoryCommand, parseOwnerPids, runBoundedProcess } from '../../src/diagnostics/ompStallSnapshot.js';
|
|
9
|
+
const ownPath = fileURLToPath(import.meta.url);
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const option = (key) => { const at = args.indexOf(key); return at < 0 ? null : args[at + 1]; };
|
|
12
|
+
const worker = process.env.UKIT_STALL_CAPTURE_WORKER === '1';
|
|
13
|
+
|
|
14
|
+
if (!worker) {
|
|
15
|
+
const child = spawn(process.execPath, [ownPath, '--worker', ...args], { stdio: ['ignore', 'pipe', 'ignore'], env: { ...process.env, UKIT_STALL_CAPTURE_WORKER: '1' } });
|
|
16
|
+
let output = '';
|
|
17
|
+
let failed = false;
|
|
18
|
+
const timer = setTimeout(() => { failed = true; child.kill('SIGKILL'); }, 5000);
|
|
19
|
+
child.stdout.on('data', (chunk) => {
|
|
20
|
+
output += chunk;
|
|
21
|
+
if (Buffer.byteLength(output) > 131072) { failed = true; child.kill('SIGKILL'); }
|
|
22
|
+
});
|
|
23
|
+
child.on('error', () => { clearTimeout(timer); console.error('Capture unavailable'); process.exitCode = 1; });
|
|
24
|
+
child.on('close', async (code) => {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
if (code !== 0 || failed) { console.error('Capture incomplete: collector deadline or bounded I/O failure; OMP untouched.'); process.exitCode = 1; return; }
|
|
27
|
+
try {
|
|
28
|
+
const report = JSON.parse(output);
|
|
29
|
+
const dir = path.resolve(option('--output-dir') || path.join(os.tmpdir(), 'ukit-stall-captures'));
|
|
30
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
31
|
+
const dirStat = await fs.lstat(dir);
|
|
32
|
+
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) throw new Error('unsafe output directory');
|
|
33
|
+
await fs.chmod(dir, 0o700);
|
|
34
|
+
let file = null;
|
|
35
|
+
let handle = null;
|
|
36
|
+
const startSlot = Date.now() % 8;
|
|
37
|
+
for (let attempt = 0; attempt < 8 && !file; attempt += 1) {
|
|
38
|
+
const slot = (startSlot + attempt) % 8;
|
|
39
|
+
const candidate = path.join(dir, `snapshot-${slot}.json`);
|
|
40
|
+
try {
|
|
41
|
+
const existing = await fs.lstat(candidate);
|
|
42
|
+
if (!existing.isFile() || existing.isSymbolicLink()) continue;
|
|
43
|
+
await fs.unlink(candidate);
|
|
44
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
45
|
+
file = candidate;
|
|
46
|
+
const temporary = path.join(dir, `snapshot-${slot}-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`);
|
|
47
|
+
try {
|
|
48
|
+
handle = await fs.open(temporary, 'wx', 0o600);
|
|
49
|
+
await handle.chmod(0o600);
|
|
50
|
+
await handle.writeFile(`${JSON.stringify(report, null, 2)}\n`);
|
|
51
|
+
await handle.close();
|
|
52
|
+
handle = null;
|
|
53
|
+
await fs.rename(temporary, file);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (handle) await handle.close();
|
|
56
|
+
await fs.unlink(temporary).catch(() => {});
|
|
57
|
+
file = null;
|
|
58
|
+
if (attempt === 7) throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!file) throw new Error('no safe snapshot slot available');
|
|
62
|
+
console.log(`${report.captureStatus} — snapshot saved: ${file}`);
|
|
63
|
+
} catch {
|
|
64
|
+
console.error('Capture incomplete: private snapshot could not be saved; OMP untouched.');
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
const maxSessions = 32;
|
|
70
|
+
const maxLogs = 8;
|
|
71
|
+
const home = option('--omp-home') || path.join(os.homedir(), '.omp');
|
|
72
|
+
const sessionsRoot = option('--sessions-root') || path.join(home, 'agent', 'sessions');
|
|
73
|
+
const ownersRoot = option('--owners-root') || path.join(home, 'run', 'session-owners');
|
|
74
|
+
const logsRoot = option('--logs-root') || path.join(home, 'logs');
|
|
75
|
+
const explicitSession = option('--session');
|
|
76
|
+
const packageInfo = JSON.parse(await fs.readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
77
|
+
|
|
78
|
+
async function discoverTranscripts(root) {
|
|
79
|
+
const candidates = [];
|
|
80
|
+
let truncated = false;
|
|
81
|
+
try {
|
|
82
|
+
const rootDir = await fs.opendir(root);
|
|
83
|
+
let visited = 0;
|
|
84
|
+
for await (const entry of rootDir) {
|
|
85
|
+
if (++visited > 128) { truncated = true; break; }
|
|
86
|
+
if (entry.isFile() && entry.name.endsWith('.jsonl')) candidates.push(path.join(root, entry.name));
|
|
87
|
+
if (!entry.isDirectory()) continue;
|
|
88
|
+
let childCount = 0;
|
|
89
|
+
try {
|
|
90
|
+
const childDir = await fs.opendir(path.join(root, entry.name));
|
|
91
|
+
for await (const child of childDir) {
|
|
92
|
+
if (++childCount > 128 || candidates.length >= maxSessions) { truncated = true; break; }
|
|
93
|
+
if (child.isFile() && child.name.endsWith('.jsonl')) candidates.push(path.join(root, entry.name, child.name));
|
|
94
|
+
}
|
|
95
|
+
} catch { truncated = true; }
|
|
96
|
+
if (candidates.length >= maxSessions) { truncated = true; break; }
|
|
97
|
+
}
|
|
98
|
+
return { state: 'OBSERVED', paths: candidates.slice(0, maxSessions), truncated };
|
|
99
|
+
} catch { return { state: 'NOT_OBSERVABLE', paths: [], truncated: true }; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function discoverOwnerIds(root) {
|
|
103
|
+
try {
|
|
104
|
+
const dir = await fs.opendir(root);
|
|
105
|
+
const ids = [];
|
|
106
|
+
let visited = 0;
|
|
107
|
+
let truncated = false;
|
|
108
|
+
for await (const entry of dir) {
|
|
109
|
+
if (++visited > 128) { truncated = true; break; }
|
|
110
|
+
if (!entry.isFile() || !entry.name.endsWith('.lock')) continue;
|
|
111
|
+
const sessionId = entry.name.slice(0, -5);
|
|
112
|
+
if (/^[\w.-]{1,120}$/.test(sessionId)) ids.push(sessionId);
|
|
113
|
+
if (ids.length >= maxSessions) { truncated = true; break; }
|
|
114
|
+
}
|
|
115
|
+
return { state: 'OBSERVED', ids, truncated };
|
|
116
|
+
} catch { return { state: 'NOT_OBSERVABLE', ids: [], truncated: true }; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runBounded(command, commandArgs, timeout = 650) {
|
|
120
|
+
return runBoundedProcess(command, commandArgs, { timeoutMs: timeout, maxBuffer: 65536 });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async function probeOwnerLock(sessionId, rootExists) {
|
|
125
|
+
if (!rootExists || !/^[\w.-]{1,120}$/.test(sessionId)) return { state: 'NOT_OBSERVABLE', pids: [] };
|
|
126
|
+
const file = path.join(ownersRoot, `${sessionId}.lock`);
|
|
127
|
+
try {
|
|
128
|
+
const stat = await fs.lstat(file);
|
|
129
|
+
if (!stat.isFile()) return { state: 'NOT_OBSERVABLE', pids: [] };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return error.code === 'ENOENT' ? { state: 'UNHELD', pids: [] } : { state: 'NOT_OBSERVABLE', pids: [] };
|
|
132
|
+
}
|
|
133
|
+
if (process.platform === 'win32') {
|
|
134
|
+
const handles = await runBounded('handle.exe', ['-nobanner', file], 400);
|
|
135
|
+
if (!handles.ok) return { state: 'NOT_OBSERVABLE', pids: [] };
|
|
136
|
+
const expected = path.resolve(file).replaceAll('/', '\\').toLowerCase();
|
|
137
|
+
const matches = handles.stdout.split(/\r?\n/).flatMap((line) => {
|
|
138
|
+
const match = line.match(/\bpid:\s*(\d+)\s+type:\s*File\b.*?:\s*(.+)$/i);
|
|
139
|
+
if (!match || match[2].trim().replaceAll('/', '\\').toLowerCase() !== expected) return [];
|
|
140
|
+
return [Number(match[1])];
|
|
141
|
+
});
|
|
142
|
+
const pids = [...new Set(matches)];
|
|
143
|
+
return pids.length === 1 ? { state: 'OBSERVED', pids } : { state: 'NOT_OBSERVABLE', pids: [] };
|
|
144
|
+
}
|
|
145
|
+
const lsof = await runBounded('lsof', ['-t', '--', file], 250);
|
|
146
|
+
if (lsof.ok) return { state: 'OBSERVED', pids: parseOwnerPids('lsof', lsof.stdout) };
|
|
147
|
+
if (lsof.exitCode === 1) return /permission|denied|error/i.test(lsof.stderr) ? { state: 'NOT_OBSERVABLE', pids: [] } : { state: 'OBSERVED', pids: [] };
|
|
148
|
+
if (lsof.errorCode !== 'ENOENT') return { state: 'NOT_OBSERVABLE', pids: [] };
|
|
149
|
+
const fuser = await runBounded('fuser', ['-f', file], 250);
|
|
150
|
+
if (fuser.ok) return { state: 'OBSERVED', pids: parseOwnerPids('fuser', `${fuser.stdout}\n${fuser.stderr}`) };
|
|
151
|
+
if (fuser.exitCode === 1) return /permission|denied|error/i.test(fuser.stderr) ? { state: 'NOT_OBSERVABLE', pids: [] } : { state: 'OBSERVED', pids: [] };
|
|
152
|
+
return { state: 'NOT_OBSERVABLE', pids: [] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async function readProcessInventory() {
|
|
157
|
+
const [command, commandArgs] = processInventoryCommand(process.platform);
|
|
158
|
+
const result = await runBounded(command, commandArgs);
|
|
159
|
+
if (!result.ok) return { state: 'NOT_OBSERVABLE', processes: [], ompProcessCount: null };
|
|
160
|
+
const processes = parseProcessInventory(process.platform, result.stdout);
|
|
161
|
+
return { state: 'OBSERVED', processes, ompProcessCount: processes.filter((entry) => /^omp(?:\.exe)?$/i.test(entry.name)).length };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function readOmpLog(pid) {
|
|
165
|
+
try {
|
|
166
|
+
const names = [];
|
|
167
|
+
const directory = await fs.opendir(logsRoot);
|
|
168
|
+
let visited = 0;
|
|
169
|
+
for await (const entry of directory) {
|
|
170
|
+
if (++visited > 256) break;
|
|
171
|
+
if (entry.isFile() && new RegExp(`^omp\\.\\d{4}-\\d{2}-\\d{2}\\.${pid}\\.log$`).test(entry.name)) names.push(entry.name);
|
|
172
|
+
}
|
|
173
|
+
names.sort();
|
|
174
|
+
const name = names.at(-1);
|
|
175
|
+
if (!name) return null;
|
|
176
|
+
const tail = await readBoundedTail(path.join(logsRoot, name), { maxBytes: 32768 });
|
|
177
|
+
const rows = [];
|
|
178
|
+
for (const line of tail.text.split('\n')) {
|
|
179
|
+
try {
|
|
180
|
+
const row = JSON.parse(line);
|
|
181
|
+
// Never copy message text, URLs, account fields, headers or response bodies.
|
|
182
|
+
const timestamp = typeof row.timestamp === 'string' && Number.isFinite(Date.parse(row.timestamp)) ? new Date(row.timestamp).toISOString() : null;
|
|
183
|
+
rows.push({ timestamp,
|
|
184
|
+
level: ['debug', 'info', 'warn', 'error'].includes(row.level) ? row.level : null,
|
|
185
|
+
status: Number.isInteger(row.status) ? row.status : null });
|
|
186
|
+
} catch { /* Partial log row. */ }
|
|
187
|
+
}
|
|
188
|
+
return { pid, bytesRead: tail.bytesRead, rows: rows.slice(-16) };
|
|
189
|
+
} catch { return null; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let discovery;
|
|
193
|
+
if (explicitSession) discovery = { state: 'OBSERVED', paths: [explicitSession] };
|
|
194
|
+
else discovery = await discoverTranscripts(sessionsRoot);
|
|
195
|
+
const transcriptSessions = [];
|
|
196
|
+
for (const transcriptPath of discovery.paths.slice(0, maxSessions)) {
|
|
197
|
+
try {
|
|
198
|
+
const tail = await readBoundedTail(transcriptPath);
|
|
199
|
+
const summary = summarizeSessionTail(tail.text);
|
|
200
|
+
const filenameId = path.basename(transcriptPath).replace(/\.jsonl$/i, '');
|
|
201
|
+
const uuidFromFilename = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(filenameId) ? filenameId : null;
|
|
202
|
+
const sessionId = summary.sessionId || uuidFromFilename;
|
|
203
|
+
const summarySessionId = summary.sessionId;
|
|
204
|
+
summary.sessionId = sessionId;
|
|
205
|
+
transcriptSessions.push({ sessionId, transcriptPath, summary, sourceCoverage: { sessionTailBytes: tail.bytesRead, truncated: tail.truncated, sessionIdentityObserved: summarySessionId !== null || uuidFromFilename !== null } });
|
|
206
|
+
} catch { transcriptSessions.push({ sessionId: null, transcriptPath, summary: null, sourceCoverage: { state: 'NOT_OBSERVABLE' } }); }
|
|
207
|
+
}
|
|
208
|
+
const ownerDiscovery = await discoverOwnerIds(ownersRoot);
|
|
209
|
+
const sessions = [...transcriptSessions];
|
|
210
|
+
for (const sessionId of ownerDiscovery.ids) if (!sessions.some((entry) => entry.sessionId === sessionId)) sessions.push({ sessionId, transcriptPath: null, summary: null, sourceCoverage: { state: 'NOT_OBSERVABLE' } });
|
|
211
|
+
const boundedSessions = sessions.slice(0, maxSessions);
|
|
212
|
+
const ownerRootExists = ownerDiscovery.state === 'OBSERVED';
|
|
213
|
+
const [ownerResults, processInventory] = await Promise.all([
|
|
214
|
+
Promise.all(boundedSessions.filter((entry) => entry.sessionId).map(async (entry) => [entry.sessionId, await probeOwnerLock(entry.sessionId, ownerRootExists)])),
|
|
215
|
+
readProcessInventory(),
|
|
216
|
+
]);
|
|
217
|
+
const ownerLocks = Object.fromEntries(ownerResults);
|
|
218
|
+
const logs = (await Promise.all([...new Set(ownerResults.flatMap(([, lock]) => lock.state === 'OBSERVED' ? lock.pids : []))].slice(0, maxLogs).map(readOmpLog))).filter(Boolean);
|
|
219
|
+
const associations = associateSessionEvidence({ sessions: boundedSessions, ownerLocks, processInventory, logs });
|
|
220
|
+
const snapshots = associations.map((association, index) => {
|
|
221
|
+
const source = boundedSessions[index];
|
|
222
|
+
const summary = source.summary;
|
|
223
|
+
const sessionActivity = classifySessionActivity({ association: association.sessionAssociation, summary });
|
|
224
|
+
return { ...association, sessionActivity, ...(summary || {}), sourceCoverage: source.sourceCoverage };
|
|
225
|
+
});
|
|
226
|
+
const activeOwners = snapshots.filter((entry) => entry.sessionAssociation === 'ACTIVE_OWNER');
|
|
227
|
+
const allLocksUnheld = ownerResults.length === boundedSessions.filter((entry) => entry.sessionId).length
|
|
228
|
+
&& ownerResults.every(([, lock]) => lock.state === 'UNHELD');
|
|
229
|
+
const coverageComplete = !discovery.truncated && !ownerDiscovery.truncated && sessions.length <= maxSessions;
|
|
230
|
+
const transcriptsFullyIdentified = transcriptSessions.every((entry) => entry.sessionId !== null);
|
|
231
|
+
const executionState = activeOwners.length ? 'EXECUTING'
|
|
232
|
+
: coverageComplete && transcriptsFullyIdentified && ownerDiscovery.state === 'OBSERVED' && processInventory.state === 'OBSERVED'
|
|
233
|
+
&& processInventory.ompProcessCount === 0 && allLocksUnheld ? 'NO_EXECUTION' : 'NOT_OBSERVABLE';
|
|
234
|
+
const activity = activeOwners.length === 1 ? activeOwners[0].sessionActivity
|
|
235
|
+
: activeOwners.length > 1 ? 'MULTIPLE_ACTIVE_SESSIONS' : executionState === 'NO_EXECUTION' ? 'NO_EXECUTION' : 'NOT_OBSERVABLE';
|
|
236
|
+
const captureStatus = activeOwners.length > 0 && activeOwners.every((entry) => entry.sourceCoverage && entry.summary)
|
|
237
|
+
&& discovery.state === 'OBSERVED' && !discovery.truncated && ownerDiscovery.state === 'OBSERVED' && !ownerDiscovery.truncated
|
|
238
|
+
&& processInventory.state === 'OBSERVED' && ownerResults.every(([, lock]) => lock.state === 'OBSERVED' || lock.state === 'UNHELD')
|
|
239
|
+
&& snapshots.every((entry) => entry.sessionId !== null && entry.identityVerified) ? 'CAPTURE_READY' : 'CAPTURE_PARTIAL';
|
|
240
|
+
const snapshot = buildStallSnapshot({ executionState, activity, captureStatus,
|
|
241
|
+
sessionDiscovery: discovery.state, ownerLockDiscovery: ownerDiscovery.state,
|
|
242
|
+
processInventory: { state: processInventory.state, ompProcessCount: processInventory.ompProcessCount },
|
|
243
|
+
sessions: snapshots, ompVersion: 'NOT_OBSERVABLE', ukitVersion: packageInfo.version,
|
|
244
|
+
limitations: ['Persisted transcript events do not prove live stream or gateway state.', 'Unobservable ownership and operation states remain NOT_OBSERVABLE.', 'No OMP freeze root cause is inferred.'] });
|
|
245
|
+
process.stdout.write(JSON.stringify(snapshot) + '\n');
|
|
246
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// DOC-102 renderer CLI (SPEC §8.2).
|
|
3
3
|
// node scripts/docs/render-instructions.mjs --write (default) regenerate all outputs
|
|
4
|
-
// node scripts/docs/render-instructions.mjs --check
|
|
4
|
+
// node scripts/docs/render-instructions.mjs --parity check installed root instructions read-only
|
|
5
|
+
// `--parity` takes precedence over `--check`/`--write`; `--parity-root` overrides only the
|
|
6
|
+
// comparison target directory for fixture/testing use, never the source/layout root.
|
|
5
7
|
import path from 'node:path';
|
|
8
|
+
import fs from 'node:fs';
|
|
6
9
|
import { fileURLToPath } from 'node:url';
|
|
7
10
|
|
|
8
11
|
import {
|
|
@@ -12,18 +15,39 @@ import {
|
|
|
12
15
|
|
|
13
16
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
14
17
|
|
|
15
|
-
const USAGE = 'usage: render-instructions.mjs [--write|--check]';
|
|
16
18
|
|
|
19
|
+
const USAGE = 'usage: render-instructions.mjs [--write|--check|--parity] [--parity-root <dir>]';
|
|
17
20
|
const args = process.argv.slice(2);
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const parityRootArg = args.indexOf('--parity-root');
|
|
22
|
+
const parityRoot = parityRootArg === -1 ? repoRoot : path.resolve(args[parityRootArg + 1] || '');
|
|
23
|
+
const unknown = args.filter((a, i) => a !== '--write' && a !== '--check' && a !== '--parity'
|
|
24
|
+
&& a !== '--parity-root' && !(parityRootArg !== -1 && i === parityRootArg + 1));
|
|
25
|
+
if (unknown.length > 0 || (parityRootArg !== -1 && !args[parityRootArg + 1])) {
|
|
26
|
+
console.error(`${USAGE}\nunknown flag or missing value: ${unknown[0] || '--parity-root'}`);
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
if (parityRootArg !== -1 && !args.includes('--parity')) {
|
|
30
|
+
console.error(`${USAGE}\n--parity-root requires --parity`);
|
|
21
31
|
process.exit(2);
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
const mode = args.includes('--check') ? 'check' : 'write';
|
|
35
|
+
const parity = args.includes('--parity');
|
|
25
36
|
|
|
26
37
|
try {
|
|
38
|
+
if (parity) {
|
|
39
|
+
const rendered = await renderInstructionsFromDisk({ repoRoot, write: false });
|
|
40
|
+
const targets = [...rendered].filter(([target]) => target === 'CLAUDE.md' || target === 'AGENTS.md');
|
|
41
|
+
const drift = targets.filter(([target, content]) => {
|
|
42
|
+
const file = path.join(parityRoot, target);
|
|
43
|
+
return !fs.existsSync(file) || !fs.readFileSync(file).equals(Buffer.from(content));
|
|
44
|
+
});
|
|
45
|
+
for (const [target] of drift) console.log(`DRIFT ${target}`);
|
|
46
|
+
if (drift.length > 0) process.exit(1);
|
|
47
|
+
console.log(`PARITY OK ${targets.length} files`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
if (mode === 'check') {
|
|
28
52
|
const drift = await checkRenderedInstructions({ repoRoot });
|
|
29
53
|
for (const target of drift) {
|
|
@@ -47,8 +47,8 @@ import {
|
|
|
47
47
|
durationMs,
|
|
48
48
|
reasonCodeForFailure,
|
|
49
49
|
projectRootFromRuntimeDir,
|
|
50
|
+
recordSubagentUsage,
|
|
50
51
|
} from './telemetry.js';
|
|
51
|
-
|
|
52
52
|
const SUPERVISOR_FLAG = 'decisionRuntime.supervisor.stage';
|
|
53
53
|
// G6 (DR-09): `decisionRuntime.promotion.stage` — absent/malformed → 'off';
|
|
54
54
|
// 'shadow' consults selectWaitPolicy at the caller-wait decision and records a
|
|
@@ -396,6 +396,13 @@ export function createSupervisor(opts = {}) {
|
|
|
396
396
|
errorCode: reasonCodeForFailure({ cancelReason: op.cancelReason ?? 'cancel_requested' }),
|
|
397
397
|
exitCode: op.exitInfo?.code,
|
|
398
398
|
});
|
|
399
|
+
if (op.spansClosed) {
|
|
400
|
+
recordSubagentUsage(telemetry, op.telemetryCtx, {
|
|
401
|
+
components: op.spec?.subagentUsage?.components,
|
|
402
|
+
parentRunId: op.spec?.parentRunId,
|
|
403
|
+
usage: op.spec?.subagentUsage?.usage,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
399
406
|
});
|
|
400
407
|
}
|
|
401
408
|
}, limits.killGraceMs));
|
|
@@ -521,6 +528,13 @@ export function createSupervisor(opts = {}) {
|
|
|
521
528
|
exitCode: op.exitInfo?.code,
|
|
522
529
|
});
|
|
523
530
|
}
|
|
531
|
+
if (op.spansClosed) {
|
|
532
|
+
recordSubagentUsage(telemetry, op.telemetryCtx, {
|
|
533
|
+
components: op.spec?.subagentUsage?.components,
|
|
534
|
+
parentRunId: op.spec?.parentRunId,
|
|
535
|
+
usage: op.spec?.subagentUsage?.usage,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
524
538
|
}
|
|
525
539
|
|
|
526
540
|
function onExit(op, code, signal) {
|
|
@@ -194,25 +194,39 @@ function nativeOperationGoal(domain, actionable, text) {
|
|
|
194
194
|
// FIRST item of an enumeration ("Do not implement fixes, modify source code, restart services, ...") and
|
|
195
195
|
// the later items became actionable orders (MIXED + service-runtime). English negators now scope to the
|
|
196
196
|
// sentence end (', and'/', or' continue the list; a bare 'and <verb>' or but/then starts a new clause);
|
|
197
|
-
// Vietnamese negators
|
|
197
|
+
// Vietnamese negators scope to the sentence end as well. Noun uses of verbs ("write/verification advisory", "each proposed
|
|
198
198
|
// fix", "state change") name things, not orders. Neutralised spans become blanks, never new orders.
|
|
199
199
|
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';
|
|
200
200
|
function neutralizeNonOrders(text) {
|
|
201
201
|
return String(text)
|
|
202
202
|
.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'), ' ')
|
|
203
|
+
.replace(new RegExp(`\\b(?:khong|chua)\\s+[^.;\\n]*?(?=\\s+(?:nhung|tuy nhien)\\b|[.;\\n]|$)`, 'g'), ' ')
|
|
204
|
+
.replace(/"[^"\n]*\s[^"\n]*"|`[^`\n]*\s[^`\n]*`|\u201c[^\u201d\n]*\u201d/g, ' ')
|
|
205
|
+
.replace(/\b(?:neu\s+(?:phai|can|thieu|khong du|bang chung chua du)|if\s+(?:needed|necessary|required|evidence is insufficient))\b[^.;\n]*/g, ' ')
|
|
206
|
+
.replace(/[^.;\n]*\b(?:nao|gi|sao|the nao|bao nhieu)\b[^.;\n]*\?/g, ' ')
|
|
207
|
+
.replace(/\bchinh\s+xac\b|\b(?:phuong an|cach|de xuat|ban)\s+(?:sua|fix)\b/g, ' ')
|
|
203
208
|
.replace(/\bwrite\s*\/\s*verification\b|\bverification\s*\/\s*write\b|\bwrite\s+(?:or|and)\s+verification\b/g, ' ')
|
|
204
209
|
.replace(/\b(?:state|status)\s+changes?\b/g, ' ')
|
|
205
210
|
.replace(/\b(?:proposed|suggested|recommended|minimal|each|the|an?|any|that|this|no|which|exact|smallest|possible)\s+fix(?:es)?\b/g, ' ');
|
|
206
211
|
}
|
|
207
212
|
|
|
208
213
|
export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
209
|
-
|
|
214
|
+
// A named preflight and both explicit branches are required; a failed arbitrary test is not a release policy.
|
|
215
|
+
const releaseText = String(promptText).normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
216
|
+
const preflight = releaseText.match(/\b(?:yarn\s+release:verify|npm\s+run\s+release:verify)\b/)?.[0];
|
|
217
|
+
const releasePolicy = preflight && /\b(?:if|neu|only when|only if)\b[^\n.;]*(?:pass|go)\b[^\n.;]*(?:release|publish)\b/.test(releaseText)
|
|
218
|
+
&& /\b(?:fail|no-go)\b[^\n.;]*(?:do not publish|don't publish|no publish|khong publish|khong npm publish)\b/.test(releaseText)
|
|
219
|
+
? { kind: 'conditional-release', preflight } : null;
|
|
220
|
+
const text = neutralizeNonOrders(String(promptText).normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase());
|
|
210
221
|
const clauses = text.split(/\s*(?:[;\n,]|\.(?:\s+|$)|\band\b|\bthen\b|\bva\b)\s*/);
|
|
211
222
|
const actionable = clauses.map((clause) => clause.split(/\bwithout\b/)[0]).filter((clause) => !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
212
223
|
&& !/^\s*(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(clause));
|
|
213
224
|
const sourceOrder = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause)
|
|
214
225
|
&& /\b(?:source|code|logic|scheduler|implementation|ma nguon)\b|\.[cm]?[jt]sx?\b/.test(clause));
|
|
215
226
|
const codeChangeVerb = actionable.some((clause) => /\b(?:implement|fix(?:es)?|apply|refactor|modify|edit|write|update|sua|viet|chinh)\b/.test(clause));
|
|
227
|
+
const reportArtifact = String(promptText).match(/(?:^|\s)((?:docs\/|reports\/)[A-Za-z0-9_./-]+\.(?:md|txt))(?=\s|[.,;:]|$)/)?.[1];
|
|
228
|
+
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))
|
|
229
|
+
&& /\b(?:report|bao cao|analysis|phan tich|document)\b/.test(text);
|
|
216
230
|
const hasOperationId = /\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/.test(text);
|
|
217
231
|
// A cron/job MENTION is not an operation: with a code-change verb in the request, an
|
|
218
232
|
// operation needs a concrete target id; otherwise it is a coding task about scheduling.
|
|
@@ -237,13 +251,13 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
237
251
|
// lookup verb AND no mutation vocabulary anywhere in the request (fail-closed otherwise).
|
|
238
252
|
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);
|
|
239
253
|
const statusReport = !mutationWord && !operation && !config
|
|
240
|
-
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua)\b/.test(text);
|
|
241
|
-
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(text) || statusReport)
|
|
254
|
+
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text);
|
|
255
|
+
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)
|
|
242
256
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
243
257
|
const taskKind = operation ? (sourceOrder || (nativeDomain !== 'scheduled-job' && codeChangeVerb) ? 'MIXED' : 'OPERATIONAL')
|
|
244
|
-
: config ? 'CONFIGURATION' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
258
|
+
: config ? 'CONFIGURATION' : documentation ? 'DOCUMENTATION_OUTPUT' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
245
259
|
const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
|
|
246
|
-
return { taskKind, ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
260
|
+
return { taskKind, ...(releasePolicy ? { releasePolicy } : {}), ...(documentation ? { reportArtifact } : {}), ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
247
261
|
...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
|
|
248
262
|
...(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',
|
|
249
263
|
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text) ? 'ensure-state' : 'execute-and-verify' } } : {}) };
|
|
@@ -254,7 +268,7 @@ export function applyTaskSemantics(contract, semantic) {
|
|
|
254
268
|
const operationOnly = semantic.taskKind === 'OPERATIONAL'
|
|
255
269
|
|| (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external');
|
|
256
270
|
return { ...(contract || {}), ...semantic,
|
|
257
|
-
completionEvidence: operationOnly ? ['operation-verification']
|
|
271
|
+
completionEvidence: semantic.taskKind === 'DOCUMENTATION_OUTPUT' ? ['report-artifact'] : operationOnly ? ['operation-verification']
|
|
258
272
|
: semantic.taskKind === 'MIXED' ? [...new Set([...evidence, 'write-evidence', 'operation-verification'])]
|
|
259
273
|
: semantic.taskKind === 'READ_ONLY' && semantic.statusReport ? []
|
|
260
274
|
: semantic.taskKind === 'READ_ONLY' ? (evidence.length === 1 && evidence[0] === 'verification-evidence' ? evidence : ['impact-evidence']) : evidence };
|