@ngockhoale/ukit 2.7.6 → 2.7.8
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 +105 -0
- package/package.json +1 -1
- package/scripts/install/sync-installed-mirror.mjs +250 -0
- package/scripts/perf/audit-perf.mjs +287 -36
- package/scripts/perf/diff-perf-findings.mjs +136 -0
- package/scripts/perf/perf-findings.json +260 -206
- package/scripts/perf/perf-measure.md +271 -0
- package/src/context/detectProjectContext.js +5 -0
- package/src/core/codeintel/invalidation.js +4 -0
- package/src/core/fileOps.js +40 -117
- package/src/core/hookChainDoctor.js +65 -2
- package/src/core/memory/store.js +22 -1
- package/src/core/taskBudgetValidator.js +9 -6
- package/src/core/unattendedDoctor.js +8 -1
- package/src/render/buildVariables.js +10 -0
- package/templates/.claude/agents/bug-debugger.md +1 -1
- package/templates/.claude/agents/feature-implementer.md +2 -2
- package/templates/.claude/commands/ukit/handoff-create.md +1 -1
- package/templates/.claude/commands/ukit/handoff-fullstack.md +1 -1
- package/templates/.claude/commands/ukit/handoff-implement.md +1 -1
- package/templates/.claude/commands/ukit/handoff-review.md +1 -1
- package/templates/.claude/hooks/auto-prune-bash.sh +19 -0
- package/templates/.claude/hooks/context-hardcap-gate.sh +4 -1
- package/templates/.claude/hooks/handoff-model-guard.sh +22 -11
- package/templates/.claude/hooks/reinject-context.sh +22 -0
- package/templates/.claude/hooks/reset-compact-pressure.sh +29 -0
- package/templates/.claude/hooks/session-episode.sh +20 -0
- package/templates/.claude/hooks/skill-router.sh +15 -8
- package/templates/.claude/hooks/verification-guard.sh +3 -0
- package/templates/.claude/ukit/index/route-task.mjs +237 -32
- package/templates/.claude/ukit/index/task-budget-validator.mjs +6 -2
- package/templates/.claude/ukit/runtime/async-lock.mjs +144 -10
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +5 -2
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +217 -17
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +156 -24
- package/templates/.claude/ukit/runtime/hook-payload-store.mjs +57 -0
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +84 -12
- package/templates/.claude/ukit/runtime/hook-telemetry.sh +50 -0
- package/templates/.claude/ukit/runtime/stop-coordinator.mjs +35 -20
- package/templates/.claude/ukit/runtime/token-utils.mjs +37 -126
- package/templates/.codex/settings.json +1 -5
- package/templates/.omp/agents/bug-debugger.md +1 -1
- package/templates/.omp/agents/feature-implementer.md +2 -2
- package/templates/.omp/hooks/pre/ukit-bridge.js +157 -26
- package/templates/docs/AI_HANDOFF/INDEX.md +1 -1
- package/templates/docs/AI_HANDOFF/RULES.md +6 -6
- package/templates/ukit/storage/config.json +2 -2
|
@@ -16,11 +16,10 @@ import {
|
|
|
16
16
|
markNotified,
|
|
17
17
|
readExecutionLedger,
|
|
18
18
|
readRouteState,
|
|
19
|
-
recordExecutionReceipt,
|
|
20
19
|
} from '../../../.claude/ukit/runtime/execution-ledger.mjs';
|
|
21
20
|
import {
|
|
22
21
|
PAYLOAD_INLINE_MAX_BYTES,
|
|
23
|
-
|
|
22
|
+
createPayloadReferenceAsync,
|
|
24
23
|
maybeSweepStalePayloads,
|
|
25
24
|
probePayloadIntegrity,
|
|
26
25
|
} from '../../../.claude/ukit/runtime/hook-payload-store.mjs';
|
|
@@ -30,6 +29,13 @@ import {
|
|
|
30
29
|
// for Edit|Write is a fail-closed transport failure = every edit blocked).
|
|
31
30
|
import { resolveChainExecTimeoutMs } from '../../../.claude/ukit/runtime/hook-chain-budget.mjs';
|
|
32
31
|
|
|
32
|
+
// OMP-3 (TASK-002): pi.exec forwards only {cwd, signal, timeout} to its spawn —
|
|
33
|
+
// an `env` option is dropped (verified against the bundled omp exec). The only
|
|
34
|
+
// channel that reaches the chain runner AND its in-proc .mjs steps is the host
|
|
35
|
+
// environment, so the bridge stamps the harness here: record-execution.mjs
|
|
36
|
+
// reads env.UKIT_HARNESS (default 'claude-code') when journaling receipts.
|
|
37
|
+
process.env.UKIT_HARNESS = 'omp';
|
|
38
|
+
|
|
33
39
|
export const HOOK_EVENT_MAP = {
|
|
34
40
|
tool_call: {
|
|
35
41
|
'Read|Grep|Glob': ['sensitive-data-guard.mjs'],
|
|
@@ -184,10 +190,18 @@ function redactDiagnosticText(text, maxChars = 500) {
|
|
|
184
190
|
|
|
185
191
|
function runtimeMetadata(event = {}, context = {}) {
|
|
186
192
|
const sessionManager = context?.sessionManager;
|
|
193
|
+
const transcriptPath = event.transcriptPath ?? event.transcript_path ?? sessionManager?.getSessionFile?.();
|
|
187
194
|
return {
|
|
188
|
-
|
|
195
|
+
// F-8: one session key for EVERY event. omp session_start events carry no
|
|
196
|
+
// session id and the context may lack sessionManager — without one,
|
|
197
|
+
// reset-compact-pressure.sh takes the unknown-caller branch and wipes EVERY
|
|
198
|
+
// session's pressure records. Falling back to the transcript path here (not
|
|
199
|
+
// per-caller) keeps the reset key identical to the key the pressure writer
|
|
200
|
+
// used on prompt/tool events that also lack an id; a per-caller fallback
|
|
201
|
+
// let the two diverge and the targeted delete silently missed.
|
|
202
|
+
sessionId: event.sessionId ?? event.session_id ?? sessionManager?.getSessionId?.() ?? transcriptPath,
|
|
189
203
|
cwd: context?.cwd ?? event.cwd,
|
|
190
|
-
transcriptPath
|
|
204
|
+
transcriptPath,
|
|
191
205
|
};
|
|
192
206
|
}
|
|
193
207
|
|
|
@@ -530,7 +544,7 @@ export async function runScriptChain(
|
|
|
530
544
|
const scriptPaths = scripts.map((scriptName) => path.join(projectRoot, '.claude', 'hooks', scriptName));
|
|
531
545
|
const nodeExecutable = resolveNodeExecutable();
|
|
532
546
|
const startedAt = Date.now();
|
|
533
|
-
const payloadReference =
|
|
547
|
+
const payloadReference = await createPayloadReferenceAsync(JSON.stringify(payload), {
|
|
534
548
|
maxBytes: PAYLOAD_INLINE_MAX_BYTES,
|
|
535
549
|
dir: payloadsDirFor(projectRoot),
|
|
536
550
|
});
|
|
@@ -767,17 +781,13 @@ export async function runToolResult(pi, event, { projectRoot, context: extension
|
|
|
767
781
|
toolUseId: event.toolCallId,
|
|
768
782
|
...metadata,
|
|
769
783
|
});
|
|
784
|
+
// OMP-3 (TASK-002): the receipt is journaled by exactly ONE path — the
|
|
785
|
+
// record-execution.mjs chain step inside runScriptChain (the same step Claude
|
|
786
|
+
// Code runs, carrying the runner's deadline/signal into the ledger lock). The
|
|
787
|
+
// former direct recordExecutionReceipt({harness:'omp'}) call here minted a
|
|
788
|
+
// second receipt per tool result, so one failed test counted as streak 2 and
|
|
789
|
+
// tripped the verification-loop blocker after a single failure.
|
|
770
790
|
const result = await runScriptChain(pi, scriptsForToolResult(toolName), payload, { projectRoot });
|
|
771
|
-
try {
|
|
772
|
-
await recordExecutionReceipt({
|
|
773
|
-
projectRoot,
|
|
774
|
-
payload,
|
|
775
|
-
toolName,
|
|
776
|
-
harness: 'omp',
|
|
777
|
-
});
|
|
778
|
-
} catch (error) {
|
|
779
|
-
pi.logger?.warn?.(`[UKit] execution receipt failed open: ${error?.message || error}`);
|
|
780
|
-
}
|
|
781
791
|
|
|
782
792
|
const hookOutput = result.context.join('\n').trim();
|
|
783
793
|
if (!hookOutput) return undefined;
|
|
@@ -849,19 +859,36 @@ export async function runSessionCompact(pi, event, { projectRoot, context: exten
|
|
|
849
859
|
// harnesses; a clean evaluation deletes it, so the count is consecutive crashes.
|
|
850
860
|
const COMPLETION_CRASH_STREAK_FILE = path.join('.ukit', 'storage', 'cache', 'completion-gate-crash.streak');
|
|
851
861
|
|
|
852
|
-
|
|
862
|
+
// OMP-1: the bridge is a long-lived in-proc module, so a streak that cannot persist
|
|
863
|
+
// (unwritable cache dir, stalled volume, lock-busy sibling writers) still advances
|
|
864
|
+
// in memory per project root. Pre-fix the catch path returned a constant 1 — the
|
|
865
|
+
// 3-crash loud release was unreachable and every omp Stop blocked forever. The
|
|
866
|
+
// in-memory count is the floor; a persisted count higher than it wins on the next
|
|
867
|
+
// successful read so cross-harness streaks still converge.
|
|
868
|
+
const crashStreakFallback = new Map();
|
|
869
|
+
|
|
870
|
+
async function bumpCompletionCrashStreak(projectRoot, pi = null) {
|
|
853
871
|
const file = path.join(projectRoot, COMPLETION_CRASH_STREAK_FILE);
|
|
854
872
|
try {
|
|
855
873
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
856
874
|
await fs.promises.appendFile(file, '.');
|
|
857
875
|
const buf = await fs.promises.readFile(file);
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
return
|
|
876
|
+
const count = Math.max(buf.length || 1, (crashStreakFallback.get(projectRoot) || 0) + 1);
|
|
877
|
+
crashStreakFallback.set(projectRoot, count);
|
|
878
|
+
return count;
|
|
879
|
+
} catch (error) {
|
|
880
|
+
const count = (crashStreakFallback.get(projectRoot) || 0) + 1;
|
|
881
|
+
crashStreakFallback.set(projectRoot, count);
|
|
882
|
+
pi?.logger?.warn?.(
|
|
883
|
+
`[UKit] completion crash streak could not persist (${error?.message || error}); `
|
|
884
|
+
+ `counting in memory (streak ${count}) so the breaker still advances.`,
|
|
885
|
+
);
|
|
886
|
+
return count;
|
|
861
887
|
}
|
|
862
888
|
}
|
|
863
889
|
|
|
864
890
|
async function resetCompletionCrashStreak(projectRoot) {
|
|
891
|
+
crashStreakFallback.delete(projectRoot);
|
|
865
892
|
try {
|
|
866
893
|
await fs.promises.rm(path.join(projectRoot, COMPLETION_CRASH_STREAK_FILE), { force: true });
|
|
867
894
|
} catch { /* best effort */ }
|
|
@@ -879,6 +906,38 @@ async function readStopGateMaxCrashStreaks(projectRoot) {
|
|
|
879
906
|
}
|
|
880
907
|
}
|
|
881
908
|
|
|
909
|
+
// ── TASK-007 (F-10/OMP-2): stop-path parity with stop-coordinator.mjs ─────────
|
|
910
|
+
// Claude's Stop runs one coordinator with three protections the omp bridge never
|
|
911
|
+
// had: the handoff-cursor lane (a non-done docs/AI_HANDOFF/RUN.md bounces the
|
|
912
|
+
// stop with the run's own Next: line, bounded by the stopGateMaxStalledBlocks
|
|
913
|
+
// liveness breaker), the 2s double-Stop dedupe window (one logical stop burns
|
|
914
|
+
// exactly one continuation), and a 600ms budget on every ledger/state lock.
|
|
915
|
+
// The evaluators are reused from stop-coordinator.mjs itself so semantics and
|
|
916
|
+
// the shared state file (.ukit/storage/cache/stop-coordinator/state.json) stay
|
|
917
|
+
// byte-identical across both engines.
|
|
918
|
+
const STOP_GATE_LOCK_BUDGET_MS = 600;
|
|
919
|
+
const HANDOFF_PROVENANCE_PREFIX = '[ukit-stop-coordinator] handoff-cursor also requested a block: ';
|
|
920
|
+
|
|
921
|
+
let stopCoordinatorModulePromise = null;
|
|
922
|
+
|
|
923
|
+
// Lazy + env-scrubbed: stop-coordinator.mjs arms a process.exit self-deadline at
|
|
924
|
+
// module top level whenever UKIT_HOOK_DEADLINE_MS is set. That env var is only
|
|
925
|
+
// ever set for spawned hook children, but the bridge is a long-lived in-proc
|
|
926
|
+
// module — a leaked value would kill the whole omp host. Same scrub the
|
|
927
|
+
// chain-runner applies around in-proc .mjs steps (hook-chain-runner.mjs:180).
|
|
928
|
+
function loadStopCoordinatorModule() {
|
|
929
|
+
if (!stopCoordinatorModulePromise) {
|
|
930
|
+
const scrubbed = 'UKIT_HOOK_DEADLINE_MS' in process.env
|
|
931
|
+
? process.env.UKIT_HOOK_DEADLINE_MS : undefined;
|
|
932
|
+
delete process.env.UKIT_HOOK_DEADLINE_MS;
|
|
933
|
+
stopCoordinatorModulePromise = import('../../../.claude/ukit/runtime/stop-coordinator.mjs')
|
|
934
|
+
.finally(() => {
|
|
935
|
+
if (scrubbed !== undefined) process.env.UKIT_HOOK_DEADLINE_MS = scrubbed;
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return stopCoordinatorModulePromise;
|
|
939
|
+
}
|
|
940
|
+
|
|
882
941
|
export async function runSessionStop(
|
|
883
942
|
pi,
|
|
884
943
|
event,
|
|
@@ -887,10 +946,69 @@ export async function runSessionStop(
|
|
|
887
946
|
context: extensionContext = {},
|
|
888
947
|
state: suppliedState,
|
|
889
948
|
ledger: suppliedLedger,
|
|
949
|
+
now = Date.now(),
|
|
950
|
+
dedupeWindowMs,
|
|
951
|
+
lockBudgetMs = STOP_GATE_LOCK_BUDGET_MS,
|
|
890
952
|
},
|
|
891
953
|
) {
|
|
892
|
-
|
|
954
|
+
// TASK-007 (OMP-2d): the stop path keys its session identity ONLY from the
|
|
955
|
+
// event itself — never the context's sessionManager fallback. omp emits
|
|
956
|
+
// session_id on every top-level session_stop (verified against the bundled
|
|
957
|
+
// binary: emitSessionStop carries session_id/session_file/stop_hook_active),
|
|
958
|
+
// so an event without one is a nested/foreign stop; attributing it to the
|
|
959
|
+
// parent session would burn the parent's continuation budget and dedupe slot.
|
|
960
|
+
const metadata = {
|
|
961
|
+
sessionId: event?.sessionId ?? event?.session_id,
|
|
962
|
+
cwd: extensionContext?.cwd ?? event?.cwd,
|
|
963
|
+
transcriptPath: event?.transcriptPath ?? event?.transcript_path ?? event?.session_file,
|
|
964
|
+
};
|
|
893
965
|
const payload = buildHookPayload('Stop', metadata);
|
|
966
|
+
|
|
967
|
+
// TASK-007 (OMP-2b): the coordinator's once-per-stop dedupe. omp can deliver
|
|
968
|
+
// two session_stop events for one logical stop; the second inside the window
|
|
969
|
+
// is skipped entirely — it must not burn a second continuation.
|
|
970
|
+
let coordinator = null;
|
|
971
|
+
try {
|
|
972
|
+
coordinator = await loadStopCoordinatorModule();
|
|
973
|
+
} catch (error) {
|
|
974
|
+
// Advisory lane: a missing/unloadable coordinator module must never wedge a
|
|
975
|
+
// session — the completion gate below still owns the stop.
|
|
976
|
+
pi.logger?.warn?.(`[UKit] stop-coordinator module unavailable (dedupe + handoff lanes skipped): ${error?.message || error}`);
|
|
977
|
+
}
|
|
978
|
+
if (coordinator) {
|
|
979
|
+
const sessionKey = typeof payload.session_id === 'string' && payload.session_id
|
|
980
|
+
? payload.session_id
|
|
981
|
+
: (typeof payload.transcript_path === 'string' && payload.transcript_path ? payload.transcript_path : 'no-session');
|
|
982
|
+
const duplicate = await coordinator.alreadyCoordinatedThisStop({
|
|
983
|
+
projectRoot,
|
|
984
|
+
sessionKey,
|
|
985
|
+
now,
|
|
986
|
+
dedupeWindowMs: dedupeWindowMs ?? coordinator.DEDUPE_WINDOW_MS,
|
|
987
|
+
lockBudgetMs,
|
|
988
|
+
});
|
|
989
|
+
if (duplicate) return undefined;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// TASK-007 (OMP-2c): the handoff-cursor lane. While docs/AI_HANDOFF/RUN.md
|
|
993
|
+
// reports a phase outside {done, blocked} the run owns this stop — the
|
|
994
|
+
// cursor's Next: line is the continuation instruction. Advisory on failure,
|
|
995
|
+
// bounded by the stopGateMaxStalledBlocks breaker (always advances post-S3).
|
|
996
|
+
let handoff = null;
|
|
997
|
+
if (coordinator) {
|
|
998
|
+
try {
|
|
999
|
+
handoff = await coordinator.evaluateHandoffCursor({ projectRoot, now, lockBudgetMs });
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
pi.logger?.warn?.(`[UKit] handoff-cursor evaluator failed (advisory lane): ${error?.message || error}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const handoffBlock = handoff?.kind === 'block' ? handoff.reason : null;
|
|
1005
|
+
// A breaker-release advisory (or any handoff systemMessage) is the omp
|
|
1006
|
+
// equivalent of the coordinator's systemMessage channel — surface it once,
|
|
1007
|
+
// visibly, whichever way the stop itself resolves.
|
|
1008
|
+
if (handoff?.kind === 'advisory' && handoff.systemMessage) {
|
|
1009
|
+
sendContext(pi, [handoff.systemMessage], 'nextTurn', { display: true });
|
|
1010
|
+
}
|
|
1011
|
+
|
|
894
1012
|
const state = suppliedState ?? await readRouteState(projectRoot, payload);
|
|
895
1013
|
const ledger = suppliedLedger ?? await readExecutionLedger(projectRoot, payload) ?? {};
|
|
896
1014
|
let evaluation;
|
|
@@ -899,8 +1017,7 @@ export async function runSessionStop(
|
|
|
899
1017
|
} catch (error) {
|
|
900
1018
|
// BUG-C22-05: fail-closed like the shell gate (continue the turn), but only up
|
|
901
1019
|
// to the configured consecutive-crash cap — a persistently broken evaluator
|
|
902
|
-
|
|
903
|
-
const streak = await bumpCompletionCrashStreak(projectRoot);
|
|
1020
|
+
const streak = await bumpCompletionCrashStreak(projectRoot, pi);
|
|
904
1021
|
const maxStreak = await readStopGateMaxCrashStreaks(projectRoot);
|
|
905
1022
|
if (streak > maxStreak) {
|
|
906
1023
|
await resetCompletionCrashStreak(projectRoot);
|
|
@@ -917,7 +1034,8 @@ export async function runSessionStop(
|
|
|
917
1034
|
continue: true,
|
|
918
1035
|
additionalContext:
|
|
919
1036
|
`UKit stop coordinator: infrastructure failure while evaluating this stop (crash streak ${streak}/${maxStreak}) — `
|
|
920
|
-
+ 'the stop is blocked fail-closed (details withheld). Run: ukit install, then re-send the task in a new message.'
|
|
1037
|
+
+ 'the stop is blocked fail-closed (details withheld). Run: ukit install, then re-send the task in a new message.'
|
|
1038
|
+
+ (handoffBlock ? `\n${HANDOFF_PROVENANCE_PREFIX}${handoffBlock}` : ''),
|
|
921
1039
|
};
|
|
922
1040
|
}
|
|
923
1041
|
// A clean evaluation resets the consecutive-crash streak (shared with the shell hook).
|
|
@@ -940,20 +1058,33 @@ export async function runSessionStop(
|
|
|
940
1058
|
// message) stays as a second guaranteed channel.
|
|
941
1059
|
sendContext(pi, [notice], 'nextTurn', { display: true });
|
|
942
1060
|
}
|
|
1061
|
+
// The completion gate released, but an in-flight handoff run still owns the
|
|
1062
|
+
// stop (coordinator merge order: handoff-cursor block outranks a released
|
|
1063
|
+
// completion). Its own stall streak — not the ledger continuation counter —
|
|
1064
|
+
// bounds this lane, so no continuation bookkeeping runs here.
|
|
1065
|
+
if (handoffBlock) {
|
|
1066
|
+
return { continue: true, additionalContext: handoffBlock };
|
|
1067
|
+
}
|
|
943
1068
|
return undefined;
|
|
944
1069
|
}
|
|
945
1070
|
|
|
946
1071
|
if (suppliedLedger === undefined) {
|
|
947
1072
|
try {
|
|
948
|
-
|
|
949
|
-
|
|
1073
|
+
// TASK-007 (OMP-2a): the 600ms lock budget — a contended ledger lock must
|
|
1074
|
+
// never hold the omp host near its hook deadline.
|
|
1075
|
+
if (evaluation.finalNotice) await markNotified(projectRoot, payload, ledger, { deadlineMs: lockBudgetMs });
|
|
1076
|
+
else await incrementContinuation(projectRoot, payload, ledger, state?.requestKey || null, evidencePromptKey(state), { deadlineMs: lockBudgetMs });
|
|
950
1077
|
} catch (error) {
|
|
951
1078
|
pi.logger?.warn?.(`[UKit] continuation bookkeeping failed open: ${error?.message || error}`);
|
|
952
1079
|
}
|
|
953
1080
|
}
|
|
954
1081
|
return {
|
|
955
1082
|
continue: true,
|
|
956
|
-
|
|
1083
|
+
// Coordinator merge order: the completion gate owns the continuation, and a
|
|
1084
|
+
// losing handoff-cursor block travels inside the winning reason.
|
|
1085
|
+
additionalContext: handoffBlock
|
|
1086
|
+
? `${evaluation.reason}\n${HANDOFF_PROVENANCE_PREFIX}${handoffBlock}`
|
|
1087
|
+
: evaluation.reason,
|
|
957
1088
|
};
|
|
958
1089
|
}
|
|
959
1090
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Status values (xem RULES.md §Status state machine):
|
|
5
5
|
ready | in_progress | pending_review | changes_requested | critical_block | approved | approved_minor | blocked | done
|
|
6
6
|
|
|
7
|
-
Owner = tool đang giữ task: claude-code |
|
|
7
|
+
Owner = tool đang giữ task: claude-code | codex | omp | -
|
|
8
8
|
-->
|
|
9
9
|
|
|
10
10
|
| ID | Title | Priority | Size | Status | Owner | Reviewer | File |
|
|
@@ -92,21 +92,21 @@ Next: <bước kế tiếp chính xác>
|
|
|
92
92
|
|
|
93
93
|
## Handoff Flow (tool-agnostic, file-based state machine)
|
|
94
94
|
|
|
95
|
-
UKit handoff hoạt động qua **file state**. Anh tự chọn tool nào cho từng phase — Claude Code /
|
|
95
|
+
UKit handoff hoạt động qua **file state**. Anh tự chọn tool nào cho từng phase — Claude Code / Codex / omp / tool mới sau này — đều được. UKit chỉ care về **role của model**, không care tool.
|
|
96
96
|
|
|
97
97
|
3 phase × 3 role model:
|
|
98
98
|
|
|
99
99
|
- **Plan** — model mạnh nhất anh có (reasoning model). Có thể chạy ở bất kỳ tool nào hỗ trợ planning tốt.
|
|
100
|
-
- **Execute** — model rẻ-mà-vẫn-thông-minh (code model). Có thể là subagent code của
|
|
101
|
-
- **Review** — **MODEL KHÁC executor** (reasoning model thường tốt hơn). Có thể là tool khác, hoặc cùng tool nhưng subagent khác model
|
|
100
|
+
- **Execute** — model rẻ-mà-vẫn-thông-minh (code model). Có thể là subagent code của omp, hay feature-implementer của Claude Code.
|
|
101
|
+
- **Review** — **MODEL KHÁC executor** (reasoning model thường tốt hơn). Có thể là tool khác, hoặc cùng tool nhưng subagent khác model.
|
|
102
102
|
|
|
103
103
|
Hai mô hình triển khai đều hợp lệ:
|
|
104
|
-
- **Cross-tool**: ví dụ Claude (plan) →
|
|
105
|
-
- **Same-tool different-subagent**: ví dụ
|
|
104
|
+
- **Cross-tool**: ví dụ Claude (plan) → Codex (execute) → Claude (review). Bridge qua file.
|
|
105
|
+
- **Same-tool different-subagent**: ví dụ omp:plan → omp:task → omp:code-reviewer, miễn 3 subagent dùng MODEL khác nhau ở role tương ứng.
|
|
106
106
|
|
|
107
107
|
Mỗi tool/subagent đọc cùng `INDEX.md` + `tasks/TASK-xxx.md` → chọn task theo `status` → cập nhật status khi xong.
|
|
108
108
|
|
|
109
|
-
> **Quan trọng — UKit không enforce model:** `handoff.executor.cheapSmartModelHint` và `handoff.reviewer.model` trong `.ukit/storage/config.json` chỉ là **nhãn** để anh biết MUỐN dùng gì. Tool nào dùng model nào là do anh chọn trong settings của tool đó. UKit enforce contract bằng cách bắt executor TỰ KHAI `EXECUTOR_MODEL` trong Executor Report; reviewer so với chính nó và refuse nếu trùng. Vì vậy nếu
|
|
109
|
+
> **Quan trọng — UKit không enforce model:** `handoff.executor.cheapSmartModelHint` và `handoff.reviewer.model` trong `.ukit/storage/config.json` chỉ là **nhãn** để anh biết MUỐN dùng gì. Tool nào dùng model nào là do anh chọn trong settings của tool đó. UKit enforce contract bằng cách bắt executor TỰ KHAI `EXECUTOR_MODEL` trong Executor Report; reviewer so với chính nó và refuse nếu trùng. Vì vậy nếu anh để cả executor-subagent và review-subagent đều dùng cùng model → reviewer sẽ tự refuse, không silent-pass.
|
|
110
110
|
|
|
111
111
|
### Status state machine
|
|
112
112
|
|
|
@@ -442,7 +442,7 @@
|
|
|
442
442
|
"field": "handoff.reviewer.model",
|
|
443
443
|
"mac_dinh": "unic-smart",
|
|
444
444
|
"y_nghia": "Model dùng cho reviewer agent ở Phase 3. BẮT BUỘC khác model executor để bắt được lỗi mà executor miss. Có thể dùng claude-opus-5, unic-smart, hoặc bất kỳ model reasoning mạnh nào.",
|
|
445
|
-
"vi_du": "Nếu executor là unic-code
|
|
445
|
+
"vi_du": "Nếu executor là unic-code, set reviewer.model=unic-smart hoặc claude-opus-5. Nếu executor là claude-sonnet, set reviewer thành claude-opus."
|
|
446
446
|
},
|
|
447
447
|
"tat_reviewer_phase": {
|
|
448
448
|
"field": "handoff.reviewer.enabled",
|
|
@@ -601,7 +601,7 @@
|
|
|
601
601
|
},
|
|
602
602
|
"handoff": {
|
|
603
603
|
"enabled": "Bật Quality Gate cho handoff: plan có Test Plan, executor test-first, reviewer model khác. Tắt = quay về flow cũ (dễ lọt lỗi vặt).",
|
|
604
|
-
"crossTool": "true nghĩa là handoff truyền qua file (PLAN/INDEX/tasks) chứ không qua in-process subagent — cho phép plan ở
|
|
604
|
+
"crossTool": "true nghĩa là handoff truyền qua file (PLAN/INDEX/tasks) chứ không qua in-process subagent — cho phép plan ở một tool, execute ở tool khác, review ở tool thứ ba khác model.",
|
|
605
605
|
"maxParallelAgents": "Số agent chạy song song TỐI ĐA trong một wave (mặc định 2 để giữ session chính nhẹ và hạn chế rủi ro compact/worktree rác; nếu máy khỏe và task độc lập nhiều thì có thể nâng dần 3-5, tối đa ~10-15). Một wave có nhiều task hơn số này sẽ được chia thành nhiều batch chạy lần lượt — áp dụng cho cả Phase 3 Implement và Phase 4 Review. Lý do giới hạn vẫn còn: mỗi agent nền có context window riêng, và report của agent khi xong sẽ được inject ngược vào session chính — chạy quá nhiều cùng lúc (vd 20+) vẫn có thể làm session chính vượt context window và bỏ lại worktree rác. Hạ xuống 1-2 nếu task nặng (verification output dài) hoặc thấy compact bị trigger liên tục.",
|
|
606
606
|
"plan": {
|
|
607
607
|
"requireTestPlan": "Bắt buộc PLAN.md §4 phải có Test Plan trước khi task chuyển ready.",
|