@ngockhoale/ukit 3.4.49 → 3.4.51
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 +18 -0
- package/manifests/documentation.yaml +22 -0
- package/package.json +1 -1
- package/src/core/executionContracts.js +36 -4
- package/src/core/reviewPanelAggregate.js +71 -3
- package/template_project/.claude/agents/code-reviewer.md +41 -6
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +43 -11
- package/template_project/.claude/commands/ukit/handoff-review.md +34 -8
- package/template_project/.claude/hooks/handoff-resume.sh +17 -3
- package/template_project/.claude/hooks/skill-router.sh +89 -11
- package/template_project/.claude/ukit/index/review-panel-aggregate.mjs +71 -3
- package/template_project/.claude/ukit/index/route-resolver.mjs +36 -4
- package/template_project/.claude/ukit/index/route-task.mjs +24 -1
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +187 -45
- package/template_project/.claude/ukit/runtime/omp-task-dispatch-guard.mjs +262 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +8 -0
- package/template_project/.omp/agents/code-reviewer.md +41 -6
- package/template_project/.omp/hooks/pre/ukit-bridge.js +38 -0
|
@@ -1965,14 +1965,46 @@ function classifyTaskSemantics({ promptText = '', targetFile = '' } = {}) {
|
|
|
1965
1965
|
if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
|
|
1966
1966
|
// A config MENTION is not a config change: the same clause needs a mutation verb, and a
|
|
1967
1967
|
// request that also changes source/code is a coding task (config is part of its tests/fixtures).
|
|
1968
|
-
|
|
1969
|
-
|
|
1968
|
+
// A DIAGNOSTIC report of past changes ("which parameters/configuration has changed, if any" /
|
|
1969
|
+
// "tham so/cau hinh nao da thay doi, neu co") is a question about history, never an authorization
|
|
1970
|
+
// to mutate: only a forward order ("change X to Y") stays a config mutation.
|
|
1971
|
+
const CONFIG_CHANGE_REPORT_CLAUSE = /\b(?:da|has|have|had|already)\s+(?:doi|thay doi|changed?|updated?)\b|\b(?:nao|gi|which|what|report|bao cao)\b[^.;]*?\b(?:doi|thay doi|changed?|updated?)\b/;
|
|
1972
|
+
// A relative past-change clause inside a FORWARD imperative ("Set the config that
|
|
1973
|
+
// has changed to dark mode") names WHICH config — it does not make the sentence a
|
|
1974
|
+
// report. The override holds only when the clause's FIRST mutation word precedes
|
|
1975
|
+
// the config noun AND no reporting/question frame opens the clause or precedes
|
|
1976
|
+
// that word: in "Report what change was made to the config" the mutation word is
|
|
1977
|
+
// a question's noun, never an order.
|
|
1978
|
+
const CONFIG_MUTATION_VERB = /\b(?:set|change|toggle|enable|disable|switch|turn|configure|modify|update|edit|doi|bat|tat|chinh)\b/;
|
|
1979
|
+
const CONFIG_NOUN = /\b(?:configuration|config|toggle|cau hinh)\b/;
|
|
1980
|
+
const CONFIG_REPORT_FRAME = /^\s*(?:report|show|tell|list|describe|explain|summari[sz]e|bao cao|cho biet|liet ke|mo ta)\b/;
|
|
1981
|
+
const CONFIG_QUESTION_FRAME_PREFIX = /\b(?:what|which|how|report|bao cao|nao|gi)\b/;
|
|
1982
|
+
const config = !sourceOrder && actionable.some((clause) => {
|
|
1983
|
+
if (!CONFIG_NOUN.test(clause) || !CONFIG_MUTATION_VERB.test(clause)) return false;
|
|
1984
|
+
if (CONFIG_REPORT_FRAME.test(clause)) return false;
|
|
1985
|
+
const verb = CONFIG_MUTATION_VERB.exec(clause);
|
|
1986
|
+
if (CONFIG_QUESTION_FRAME_PREFIX.test(clause.slice(0, verb.index))) return false;
|
|
1987
|
+
const noun = CONFIG_NOUN.exec(clause);
|
|
1988
|
+
return !CONFIG_CHANGE_REPORT_CLAUSE.test(clause) || Boolean(noun && noun.index > verb.index);
|
|
1989
|
+
});
|
|
1970
1990
|
const repository = Boolean(targetFile) || /[\w.-]+\.(?:json|ya?ml|toml|ini)\b/.test(text);
|
|
1971
1991
|
// A status/result lookup ("check the suite output and report") orders no change: it needs a
|
|
1972
1992
|
// lookup verb AND no mutation vocabulary anywhere in the request (fail-closed otherwise).
|
|
1973
1993
|
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);
|
|
1974
|
-
|
|
1975
|
-
|
|
1994
|
+
// A bounded, recognizable reporting QUESTION about what changed ("Tham số/cấu hình
|
|
1995
|
+
// nào đã thay đổi, nếu có." / "which configuration has changed, if any") is a
|
|
1996
|
+
// status report even without a lookup verb — but ONLY when no actionable clause
|
|
1997
|
+
// actually orders a mutation: a mixed request ("Add a dark-mode button; report
|
|
1998
|
+
// which configuration changed") keeps its implementation contract and evidence.
|
|
1999
|
+
// The two lookbehinds keep a question's noun ("what change was made") and the
|
|
2000
|
+
// Vietnamese past-tense verb "thay đổi" from counting as mutation orders.
|
|
2001
|
+
const ACTIONABLE_MUTATION_ORDER = /(?<!\b(?:what|which|how|nao|gi)\s)(?<!thay )\b(?:implement|fix(?:es)?|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/;
|
|
2002
|
+
const configReportQuestion = !actionable.some((clause) => ACTIONABLE_MUTATION_ORDER.test(clause))
|
|
2003
|
+
&& actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh|tham so)\b/.test(clause)
|
|
2004
|
+
&& CONFIG_CHANGE_REPORT_CLAUSE.test(clause));
|
|
2005
|
+
const statusReport = (configReportQuestion || (!mutationWord
|
|
2006
|
+
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text)))
|
|
2007
|
+
&& !operation && !config;
|
|
1976
2008
|
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)
|
|
1977
2009
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
1978
2010
|
const taskKind = operation ? (sourceOrder || (nativeDomain !== 'scheduled-job' && codeChangeVerb) ? 'MIXED' : 'OPERATIONAL')
|
|
@@ -3346,6 +3378,14 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3346
3378
|
ts: Date.now(),
|
|
3347
3379
|
source: state?.source || 'skill-router',
|
|
3348
3380
|
requestKey: state?.requestKey || null,
|
|
3381
|
+
// Continuation-identity audit join: which task this row served and where its
|
|
3382
|
+
// id/contract came from (gate continuation vs fresh route). Telemetry only —
|
|
3383
|
+
// deliberately excluded from the dedupe key. The hook passes the PERSISTED
|
|
3384
|
+
// route state here, so these mirror the state's own audit stamps.
|
|
3385
|
+
taskInstanceId: state?.taskInstanceId || null,
|
|
3386
|
+
taskIdOrigin: state?.taskIdOrigin || null,
|
|
3387
|
+
taskKind: routeSummary?.executionContract?.taskKind || null,
|
|
3388
|
+
contractOrigin: state?.contractOrigin || null,
|
|
3349
3389
|
adapter: routingContext.adapter || 'claude',
|
|
3350
3390
|
promptFingerprint: stableMachineDigest({
|
|
3351
3391
|
prompt: routingContext.lastExplicitUserPromptText || '',
|
|
@@ -3844,6 +3884,10 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3844
3884
|
async function persistRouteState(text) {
|
|
3845
3885
|
const persisted = JSON.parse(text);
|
|
3846
3886
|
persisted.taskInstanceId = taskInstanceId;
|
|
3887
|
+
// Bounded audit metadata (short strings, additive): where this turn's task id and
|
|
3888
|
+
// contract came from — 'system' marks a synthetic continuation that resumed the
|
|
3889
|
+
// previous task instead of minting a fresh one. No consumer branches on these yet.
|
|
3890
|
+
persisted.taskIdOrigin = continuingTask ? (systemOriginTurn ? 'system' : 'continue') : 'fresh';
|
|
3847
3891
|
if (continuingTask && previous.routingContext?.lastExplicitUserPromptText) {
|
|
3848
3892
|
persisted.routingContext ||= {};
|
|
3849
3893
|
persisted.routingContext.lastExplicitUserPromptText = previous.routingContext.lastExplicitUserPromptText;
|
|
@@ -3862,6 +3906,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3862
3906
|
// original request's, never one re-derived from the injected prompt's wording.
|
|
3863
3907
|
persisted.routeSummary ||= {};
|
|
3864
3908
|
persisted.routeSummary.executionContract = previous.routeSummary.executionContract;
|
|
3909
|
+
persisted.contractOrigin = 'carried-system';
|
|
3865
3910
|
for (const key of ['expectedSourceFiles', 'completionState', 'continuationState', 'executionMode']) {
|
|
3866
3911
|
if (previous.routeSummary[key] !== undefined) persisted.routeSummary[key] = previous.routeSummary[key];
|
|
3867
3912
|
else delete persisted.routeSummary[key];
|
|
@@ -3896,6 +3941,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3896
3941
|
persisted.routeSummary ||= { executionMode: 'local-build' };
|
|
3897
3942
|
persisted.routeSummary.executionContract = applyTaskSemantics(
|
|
3898
3943
|
persisted.routeSummary.executionContract || buildExecutionContract('local-build'), semantic);
|
|
3944
|
+
if (persisted.contractOrigin !== 'carried-system') persisted.contractOrigin = 'classified';
|
|
3899
3945
|
delete persisted.routeSummary.completionState;
|
|
3900
3946
|
delete persisted.routeSummary.continuationState;
|
|
3901
3947
|
} else if (semantic.taskKind !== 'CODE_IMPLEMENTATION' || persisted.routeSummary) {
|
|
@@ -3906,8 +3952,11 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3906
3952
|
if (carriedStatus && persisted.routingContext?.mutationOrder === true) {
|
|
3907
3953
|
delete persisted.routeSummary.executionContract;
|
|
3908
3954
|
}
|
|
3909
|
-
if (!carriedStatus || persisted.routingContext?.mutationOrder === true)
|
|
3910
|
-
persisted.routeSummary.executionContract
|
|
3955
|
+
if (!carriedStatus || persisted.routingContext?.mutationOrder === true) {
|
|
3956
|
+
persisted.routeSummary.executionContract = applyTaskSemantics(
|
|
3957
|
+
persisted.routeSummary.executionContract || buildExecutionContract(persisted.routeSummary.executionMode === 'informational' ? 'local-build' : persisted.routeSummary.executionMode), semantic);
|
|
3958
|
+
if (persisted.contractOrigin !== 'carried-system') persisted.contractOrigin = 'classified';
|
|
3959
|
+
}
|
|
3911
3960
|
if (semantic.taskKind === 'OPERATIONAL' || semantic.taskKind === 'READ_ONLY'
|
|
3912
3961
|
|| (semantic.taskKind === 'CONFIGURATION' && semantic.configurationScope === 'external')) {
|
|
3913
3962
|
persisted.routeSummary.completionState = buildCompletionState({
|
|
@@ -3925,7 +3974,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3925
3974
|
}
|
|
3926
3975
|
text = JSON.stringify(persisted);
|
|
3927
3976
|
await writeJsonFileAtomic(statePath, text);
|
|
3928
|
-
if (!sessionId) return;
|
|
3977
|
+
if (!sessionId) return persisted;
|
|
3929
3978
|
try {
|
|
3930
3979
|
const dir = path.join(projectRoot, '.ukit', 'storage', 'cache', 'route-state');
|
|
3931
3980
|
await writeJsonFileAtomic(path.join(dir, `${String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || 'default'}.json`), text);
|
|
@@ -3938,6 +3987,7 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3938
3987
|
}
|
|
3939
3988
|
}
|
|
3940
3989
|
} catch {}
|
|
3990
|
+
return persisted;
|
|
3941
3991
|
}
|
|
3942
3992
|
const storedPrevious = await readJson(statePath, {});
|
|
3943
3993
|
// The shared slot may belong to another session; recover only our owned sidecar.
|
|
@@ -3967,10 +4017,38 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
3967
4017
|
// verification-only turn can never satisfy). Only transcript metadata set by the harness or the
|
|
3968
4018
|
// fixed compaction preamble counts - never the prompt's claims about itself.
|
|
3969
4019
|
let coordinationTurn = false;
|
|
4020
|
+
// Provenance: our OWN gate block re-injected as a prompt. Trust only the exact pending marker
|
|
4021
|
+
// the gate recorded for this session + task + EXACT reason text (hash) within TTL (consumed on
|
|
4022
|
+
// match), and only when the host did not declare the prompt user-originated: a user quoting the
|
|
4023
|
+
// gate text is a genuine fresh request even while a marker is still fresh. Ledger module loads
|
|
4024
|
+
// only for gate-shaped prompts, so ordinary turns pay nothing.
|
|
4025
|
+
const promptOrigin = String(payload.ukit_prompt_origin ?? '').trim().toLowerCase();
|
|
4026
|
+
let boundGateContinuation = false;
|
|
4027
|
+
{
|
|
4028
|
+
const gatePrompt = extractPrompt(payload).trim();
|
|
4029
|
+
if (gatePrompt && sessionId && promptOrigin === 'system' && /^UKit completion gate:/.test(gatePrompt)) {
|
|
4030
|
+
try {
|
|
4031
|
+
const ledgerRuntime = await import(pathToFileURL(path.join(projectRoot, '.claude', 'ukit', 'runtime', 'execution-ledger.mjs')).href);
|
|
4032
|
+
const gateTaskId = ledgerRuntime.completionContinuationTaskId(gatePrompt);
|
|
4033
|
+
if (gateTaskId && previous.taskInstanceId === gateTaskId) {
|
|
4034
|
+
const taken = await ledgerRuntime.takePendingCompletionContinuation(projectRoot, {
|
|
4035
|
+
sessionId,
|
|
4036
|
+
taskInstanceId: gateTaskId,
|
|
4037
|
+
reasonHash: ledgerRuntime.completionContinuationReasonHash(gatePrompt),
|
|
4038
|
+
});
|
|
4039
|
+
boundGateContinuation = Boolean(taken);
|
|
4040
|
+
}
|
|
4041
|
+
} catch { boundGateContinuation = false; }
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
3970
4044
|
const systemOriginTurn = (() => {
|
|
3971
4045
|
const promptTrim = extractPrompt(payload).trim();
|
|
3972
4046
|
if (!promptTrim) return false;
|
|
3973
4047
|
if (/^This session is being continued from a previous conversation/i.test(promptTrim)) return true;
|
|
4048
|
+
// Bridge-owned metadata only (ukit-bridge.js maps the host's session-stop-continuation
|
|
4049
|
+
// attribution); an arbitrary prompt prefix proves nothing and is never trusted here.
|
|
4050
|
+
if (payload.ukit_harness === 'omp' && promptOrigin === 'system') return true;
|
|
4051
|
+
if (boundGateContinuation) return true;
|
|
3974
4052
|
const tp = payload.transcript_path ?? payload.transcriptPath;
|
|
3975
4053
|
if (!tp || typeof tp !== 'string') return false;
|
|
3976
4054
|
try {
|
|
@@ -4392,11 +4470,11 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4392
4470
|
}
|
|
4393
4471
|
|
|
4394
4472
|
await ensureDir(statePath);
|
|
4395
|
-
await persistRouteState(JSON.stringify(reusedState));
|
|
4473
|
+
const persistedReused = await persistRouteState(JSON.stringify(reusedState));
|
|
4396
4474
|
await appendRouteAuditEntry(routeAuditPath, buildRouteAuditEntry({
|
|
4397
4475
|
routingContext: reusedState.routingContext || {},
|
|
4398
4476
|
routeSummary: rescuedRouteSummary || {},
|
|
4399
|
-
state: reusedState,
|
|
4477
|
+
state: persistedReused || reusedState,
|
|
4400
4478
|
}));
|
|
4401
4479
|
|
|
4402
4480
|
// BL-017: detached decisions.tsv shadow emission for the reused route —
|
|
@@ -4533,11 +4611,11 @@ function applyTaskSemantics(contract, semantic) {
|
|
|
4533
4611
|
...(helpers ? { helpers } : {}),
|
|
4534
4612
|
routeSummary: compactRouteSummary(routeSummary),
|
|
4535
4613
|
};
|
|
4536
|
-
await persistRouteState(JSON.stringify(sharedState));
|
|
4614
|
+
const persistedRouteState = await persistRouteState(JSON.stringify(sharedState));
|
|
4537
4615
|
await appendRouteAuditEntry(routeAuditPath, buildRouteAuditEntry({
|
|
4538
4616
|
routingContext,
|
|
4539
4617
|
routeSummary,
|
|
4540
|
-
state: sharedState,
|
|
4618
|
+
state: persistedRouteState || sharedState,
|
|
4541
4619
|
}));
|
|
4542
4620
|
|
|
4543
4621
|
// BL-017: detached decisions.tsv shadow emission — fire-and-forget child
|
|
@@ -30,12 +30,73 @@ const SEVERITIES = ['critical', 'important', 'minor'];
|
|
|
30
30
|
// counted as absent and named in `unparsed`.
|
|
31
31
|
export function parseVerdictBlock(text) {
|
|
32
32
|
const src = String(text ?? '');
|
|
33
|
-
|
|
33
|
+
// `-` and `_` are one vocabulary: the handoff commands and some reviewers
|
|
34
|
+
// write `changes_requested` / `critical_block` while others write the hyphen
|
|
35
|
+
// forms — both spellings parse to the same canonical verdict so a blocking
|
|
36
|
+
// reviewer can never be dropped as unparsed while the panel approves.
|
|
37
|
+
// `[ \t]` (not `\s`) around the colon: `\s` would swallow the newline and
|
|
38
|
+
// let a stray word on the next line fabricate a verdict.
|
|
39
|
+
const verdictMatch = src.match(
|
|
40
|
+
/^VERDICT[ \t]*:[ \t]*(APPROVED[-_]WITH[-_]MINOR|APPROVED|CHANGES[-_]REQUESTED|CRITICAL[-_]BLOCK|CRITICAL)[ \t]*\r?$/im,
|
|
41
|
+
);
|
|
34
42
|
if (!verdictMatch) return null;
|
|
43
|
+
const rawVerdict = verdictMatch[1].toUpperCase().replace(/_/g, '-');
|
|
44
|
+
const verdict = rawVerdict === 'CRITICAL-BLOCK' ? 'CRITICAL' : rawVerdict;
|
|
35
45
|
|
|
36
46
|
const memberMatch = src.match(/^PANEL_MEMBER\s*:\s*(\S+)\s*$/im);
|
|
37
47
|
const member = memberMatch && memberMatch[1] !== '-' ? memberMatch[1] : null;
|
|
38
48
|
|
|
49
|
+
// VERIFICATION_RERUN status (PASS | FAIL | NOT_RUN | N/A). The status may
|
|
50
|
+
// sit inline on the heading line AND/OR under the indented `result:` line —
|
|
51
|
+
// both positions are read, and an explicit FAIL or NOT_RUN in either wins
|
|
52
|
+
// over a PASS elsewhere (fail closed on contradiction). Bare prose
|
|
53
|
+
// (`N pass / M fail`) and a literal template echo (`VERIFICATION_RERUN:
|
|
54
|
+
// PASS | FAIL | …` — still carrying the `|` vocabulary) parse as no status,
|
|
55
|
+
// leaving the field null (= no required metadata → pre-existing behavior).
|
|
56
|
+
// FAIL = the re-run executed and failed; NOT_RUN = the required verification
|
|
57
|
+
// never executed — either blocks approval.
|
|
58
|
+
//
|
|
59
|
+
// Layout is read exhaustively and conservatively: `[ \t]` (not `\s`) after
|
|
60
|
+
// the heading colon so the match cannot swallow the newline before an
|
|
61
|
+
// indented child (a `result:` FIRST child used to be hidden this way), the
|
|
62
|
+
// global match reads REPEATED VERIFICATION_RERUN headers, and every
|
|
63
|
+
// `result:` child line of each header is collected so a contradictory
|
|
64
|
+
// second line cannot hide behind the first. FAIL/NOT_RUN dominate.
|
|
65
|
+
const STATUS_RANK = { FAIL: 3, NOT_RUN: 2, PASS: 1, 'N/A': 0 };
|
|
66
|
+
const statuses = [];
|
|
67
|
+
const commands = [];
|
|
68
|
+
const vrRe = /^VERIFICATION_RERUN[ \t]*:[ \t]*(.*)$/img;
|
|
69
|
+
let vr;
|
|
70
|
+
while ((vr = vrRe.exec(src)) !== null) {
|
|
71
|
+
const inline = vr[1].trim();
|
|
72
|
+
const inlineStatus = inline.includes('|') ? null : inline.match(/^(PASS|FAIL|NOT_RUN|N\/A)\b/i);
|
|
73
|
+
if (inlineStatus) statuses.push(inlineStatus[1].toUpperCase());
|
|
74
|
+
for (const line of src.slice(vr.index + vr[0].length).split('\n')) {
|
|
75
|
+
if (/^\S/.test(line)) break; // next top-level field ends this header's children
|
|
76
|
+
const result = line.match(/^[ \t]*result[ \t]*:[ \t]*(.*)$/i);
|
|
77
|
+
if (result) {
|
|
78
|
+
const status = result[1].trim().match(/^(PASS|FAIL|NOT_RUN|N\/A)\b/i);
|
|
79
|
+
if (status) statuses.push(status[1].toUpperCase());
|
|
80
|
+
continue; // keep reading — a contradictory later `result:` must still block
|
|
81
|
+
}
|
|
82
|
+
const command = line.match(/^[ \t]*command[ \t]*:[ \t]*(.*)$/i);
|
|
83
|
+
if (command) commands.push(command[1].trim());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const dominant = statuses.length > 0
|
|
87
|
+
? [...statuses].sort((a, b) => STATUS_RANK[b] - STATUS_RANK[a])[0]
|
|
88
|
+
: null;
|
|
89
|
+
// Fail closed on applicability: an N/A claim beside a DECLARED executable
|
|
90
|
+
// command contradicts the block's own evidence — the required check exists,
|
|
91
|
+
// so the N/A counts as NOT_RUN (the check was not run). A placeholder
|
|
92
|
+
// command (`-`, `none`, `(none …)`, `n/a …`) or no command line keeps N/A
|
|
93
|
+
// legitimate for genuinely check-free reviews.
|
|
94
|
+
const hasExecutableCommand = commands.some((c) => {
|
|
95
|
+
const t = c.trim().toLowerCase();
|
|
96
|
+
return t !== '' && t !== '-' && t !== 'none' && !t.startsWith('(none') && !t.startsWith('n/a');
|
|
97
|
+
});
|
|
98
|
+
const verificationRerun = dominant === 'N/A' && hasExecutableCommand ? 'NOT_RUN' : dominant;
|
|
99
|
+
|
|
39
100
|
const findings = { critical: [], important: [], minor: [] };
|
|
40
101
|
const findingsMatch = src.match(/^FINDINGS:\s*$/im);
|
|
41
102
|
if (findingsMatch) {
|
|
@@ -70,7 +131,7 @@ export function parseVerdictBlock(text) {
|
|
|
70
131
|
}
|
|
71
132
|
}
|
|
72
133
|
|
|
73
|
-
return { member, verdict
|
|
134
|
+
return { member, verdict, findings, verificationRerun };
|
|
74
135
|
}
|
|
75
136
|
|
|
76
137
|
// Normalize a finding bullet for identity comparison: two members report the
|
|
@@ -120,8 +181,15 @@ export function aggregatePanelVerdicts(verdicts) {
|
|
|
120
181
|
const hasCritical =
|
|
121
182
|
parsed.some((p) => p.verdict === 'CRITICAL') || parsed.some((p) => p.findings.critical.length > 0);
|
|
122
183
|
|
|
184
|
+
// Fail closed on explicit verification evidence: a re-run that FAILED, or
|
|
185
|
+
// one never executed (NOT_RUN), cannot sit behind an approving prose
|
|
186
|
+
// verdict — the block stops counting toward approval entirely.
|
|
187
|
+
const hasBlockingVerification = parsed.some(
|
|
188
|
+
(p) => p.verificationRerun === 'FAIL' || p.verificationRerun === 'NOT_RUN',
|
|
189
|
+
);
|
|
190
|
+
|
|
123
191
|
let verdict;
|
|
124
|
-
if (hasCritical || changeRequests >= 1 || parsed.length === 0) verdict = 'changes_requested';
|
|
192
|
+
if (hasCritical || changeRequests >= 1 || hasBlockingVerification || parsed.length === 0) verdict = 'changes_requested';
|
|
125
193
|
else if (approvals >= 2) verdict = 'approved';
|
|
126
194
|
else verdict = 'approved_minor';
|
|
127
195
|
|
|
@@ -1202,14 +1202,46 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1202
1202
|
if (!releasePolicy && !operation && isVerificationOnlyRequest(promptText)) return { taskKind: 'READ_ONLY', verificationOnly: true };
|
|
1203
1203
|
// A config MENTION is not a config change: the same clause needs a mutation verb, and a
|
|
1204
1204
|
// request that also changes source/code is a coding task (config is part of its tests/fixtures).
|
|
1205
|
-
|
|
1206
|
-
|
|
1205
|
+
// A DIAGNOSTIC report of past changes ("which parameters/configuration has changed, if any" /
|
|
1206
|
+
// "tham so/cau hinh nao da thay doi, neu co") is a question about history, never an authorization
|
|
1207
|
+
// to mutate: only a forward order ("change X to Y") stays a config mutation.
|
|
1208
|
+
const CONFIG_CHANGE_REPORT_CLAUSE = /\b(?:da|has|have|had|already)\s+(?:doi|thay doi|changed?|updated?)\b|\b(?:nao|gi|which|what|report|bao cao)\b[^.;]*?\b(?:doi|thay doi|changed?|updated?)\b/;
|
|
1209
|
+
// A relative past-change clause inside a FORWARD imperative ("Set the config that
|
|
1210
|
+
// has changed to dark mode") names WHICH config — it does not make the sentence a
|
|
1211
|
+
// report. The override holds only when the clause's FIRST mutation word precedes
|
|
1212
|
+
// the config noun AND no reporting/question frame opens the clause or precedes
|
|
1213
|
+
// that word: in "Report what change was made to the config" the mutation word is
|
|
1214
|
+
// a question's noun, never an order.
|
|
1215
|
+
const CONFIG_MUTATION_VERB = /\b(?:set|change|toggle|enable|disable|switch|turn|configure|modify|update|edit|doi|bat|tat|chinh)\b/;
|
|
1216
|
+
const CONFIG_NOUN = /\b(?:configuration|config|toggle|cau hinh)\b/;
|
|
1217
|
+
const CONFIG_REPORT_FRAME = /^\s*(?:report|show|tell|list|describe|explain|summari[sz]e|bao cao|cho biet|liet ke|mo ta)\b/;
|
|
1218
|
+
const CONFIG_QUESTION_FRAME_PREFIX = /\b(?:what|which|how|report|bao cao|nao|gi)\b/;
|
|
1219
|
+
const config = !sourceOrder && actionable.some((clause) => {
|
|
1220
|
+
if (!CONFIG_NOUN.test(clause) || !CONFIG_MUTATION_VERB.test(clause)) return false;
|
|
1221
|
+
if (CONFIG_REPORT_FRAME.test(clause)) return false;
|
|
1222
|
+
const verb = CONFIG_MUTATION_VERB.exec(clause);
|
|
1223
|
+
if (CONFIG_QUESTION_FRAME_PREFIX.test(clause.slice(0, verb.index))) return false;
|
|
1224
|
+
const noun = CONFIG_NOUN.exec(clause);
|
|
1225
|
+
return !CONFIG_CHANGE_REPORT_CLAUSE.test(clause) || Boolean(noun && noun.index > verb.index);
|
|
1226
|
+
});
|
|
1207
1227
|
const repository = Boolean(targetFile) || /[\w.-]+\.(?:json|ya?ml|toml|ini)\b/.test(text);
|
|
1208
1228
|
// A status/result lookup ("check the suite output and report") orders no change: it needs a
|
|
1209
1229
|
// lookup verb AND no mutation vocabulary anywhere in the request (fail-closed otherwise).
|
|
1210
1230
|
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);
|
|
1211
|
-
|
|
1212
|
-
|
|
1231
|
+
// A bounded, recognizable reporting QUESTION about what changed ("Tham số/cấu hình
|
|
1232
|
+
// nào đã thay đổi, nếu có." / "which configuration has changed, if any") is a
|
|
1233
|
+
// status report even without a lookup verb — but ONLY when no actionable clause
|
|
1234
|
+
// actually orders a mutation: a mixed request ("Add a dark-mode button; report
|
|
1235
|
+
// which configuration changed") keeps its implementation contract and evidence.
|
|
1236
|
+
// The two lookbehinds keep a question's noun ("what change was made") and the
|
|
1237
|
+
// Vietnamese past-tense verb "thay đổi" from counting as mutation orders.
|
|
1238
|
+
const ACTIONABLE_MUTATION_ORDER = /(?<!\b(?:what|which|how|nao|gi)\s)(?<!thay )\b(?:implement|fix(?:es)?|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/;
|
|
1239
|
+
const configReportQuestion = !actionable.some((clause) => ACTIONABLE_MUTATION_ORDER.test(clause))
|
|
1240
|
+
&& actionable.some((clause) => /\b(?:configuration|config|toggle|cau hinh|tham so)\b/.test(clause)
|
|
1241
|
+
&& CONFIG_CHANGE_REPORT_CLAUSE.test(clause));
|
|
1242
|
+
const statusReport = (configReportQuestion || (!mutationWord
|
|
1243
|
+
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua|da len chua)\b/.test(text)))
|
|
1244
|
+
&& !operation && !config;
|
|
1213
1245
|
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.replace(/\bread[\s-]*only\b/g, ' ')) || statusReport)
|
|
1214
1246
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
1215
1247
|
const taskKind = operation ? (sourceOrder || codeChangeVerb ? 'MIXED' : 'OPERATIONAL')
|
|
@@ -1131,13 +1131,29 @@ const HOOK_STATE_SOURCES = ['skill-router', 'skill-router-provisional'];
|
|
|
1131
1131
|
* or any state the hook did not own, so a repeated request still mints a new id and unrelated evidence
|
|
1132
1132
|
* stays rejected. An explicit --task-instance-id is honored when well-formed.
|
|
1133
1133
|
*/
|
|
1134
|
+
// Mirror of execution-ledger.mjs isCompletionGateContinuationText (kept local so the CLI stays
|
|
1135
|
+
// import-free). Shape only: trust is decided by the caller's bound pending marker, never by this match.
|
|
1136
|
+
const GATE_CONTINUATION_PREFIX = /^UKit completion gate:/;
|
|
1137
|
+
const GATE_CONTINUATION_DIAGNOSTICS = /\s\[task=\S+ contract=\S+ accepted-count=\d+ rejected=[^\]\s]*\]\s*$/;
|
|
1138
|
+
export function isCompletionGateContinuationText(text) {
|
|
1139
|
+
const value = String(text ?? '').trim();
|
|
1140
|
+
if (!value || value.length > 2000) return false;
|
|
1141
|
+
return GATE_CONTINUATION_PREFIX.test(value) && GATE_CONTINUATION_DIAGNOSTICS.test(value);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1134
1144
|
export function reuseOrMintTaskInstanceId({ previousState = {}, promptText = '', commandText = '', targetFile = null, explicitId = null, continuingTask = false } = {}) {
|
|
1135
1145
|
const explicit = typeof explicitId === 'string' ? explicitId.trim() : '';
|
|
1136
1146
|
if (explicit && TASK_INSTANCE_ID_SHAPE.test(explicit)) return { id: explicit, origin: null };
|
|
1137
1147
|
const prevId = previousState?.taskInstanceId;
|
|
1138
1148
|
if (prevId) {
|
|
1149
|
+
// 'system' (a gate continuation the HOOK verified via positive provenance —
|
|
1150
|
+
// marker or native origin metadata — and stamped into route state) keeps the
|
|
1151
|
+
// same-prompt refresh rights as a hook-owned id: the task must persist across
|
|
1152
|
+
// the synthetic continuation, resolve-context and tool receipts until a real
|
|
1153
|
+
// new request. The CLI itself is not a provenance source: it only honors the
|
|
1154
|
+
// origin the hook already established.
|
|
1139
1155
|
const hookTurn = HOOK_STATE_SOURCES.includes(previousState.source)
|
|
1140
|
-
|| (previousState.source === 'task-route' && previousState.taskIdOrigin === 'hook');
|
|
1156
|
+
|| (previousState.source === 'task-route' && (previousState.taskIdOrigin === 'hook' || previousState.taskIdOrigin === 'system'));
|
|
1141
1157
|
const stored = previousState.routingContext?.lastExplicitUserPromptText;
|
|
1142
1158
|
const samePrompt = Boolean(stored) && stored === promptText;
|
|
1143
1159
|
const promptlessRefresh = Boolean(stored) && !promptText && Boolean(commandText || targetFile);
|
|
@@ -6125,6 +6141,13 @@ function buildRouteAuditEntry({ route = null, state = null } = {}) {
|
|
|
6125
6141
|
fastPathKind: routeSummary.fastPathKind ?? null,
|
|
6126
6142
|
source: state?.source ?? 'task-route',
|
|
6127
6143
|
requestKey: state?.requestKey ?? null,
|
|
6144
|
+
// Continuation-identity audit join: which task this row served and where its
|
|
6145
|
+
// id/contract came from (gate continuation vs fresh route). Telemetry only —
|
|
6146
|
+
// deliberately excluded from dedupeKey/fingerprint inputs.
|
|
6147
|
+
taskInstanceId: state?.taskInstanceId ?? null,
|
|
6148
|
+
taskIdOrigin: state?.taskIdOrigin ?? null,
|
|
6149
|
+
taskKind: routeSummary?.executionContract?.taskKind ?? null,
|
|
6150
|
+
contractOrigin: state?.contractOrigin ?? null,
|
|
6128
6151
|
adapter: routingContext.adapter ?? 'claude',
|
|
6129
6152
|
promptFingerprint: stableMachineDigest({
|
|
6130
6153
|
prompt: routingContext.lastExplicitUserPromptText ?? '',
|