@ngockhoale/ukit 3.4.42 → 3.4.44
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 +17 -0
- package/manifests/documentation.yaml +11 -0
- package/manifests/instructionRules.yaml +4 -1
- package/package.json +1 -1
- package/template_project/.claude/ukit/index/route-resolver.mjs +5 -2
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +36 -10
- package/template_project/.claude/ukit/runtime/operation-evidence.mjs +16 -2
- package/template_project/AGENTS.md +1 -0
- package/template_project/CLAUDE.md +1 -0
- package/template_project/instructions/core.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,23 @@ All notable changes to UKit are documented here.
|
|
|
9
9
|
- New `/ukit:ci-check` command + `ci-check.mjs` helper (core pack): prechecks, fail-closed budget guard, local preflight, at most one dispatch per invocation, write-ahead run-id state, `status` (never dispatches), `--ref`, `--dry-run`, per-OS report. Never pushes, merges or publishes.
|
|
10
10
|
- Default-branch bootstrap stub documented in `docs/design/CI-CROSS-PLATFORM-MATRIX.md` (requires separate approval).
|
|
11
11
|
|
|
12
|
+
## 3.4.44 - 2026-10-14
|
|
13
|
+
|
|
14
|
+
**Completion Gate scope and permission-choice preservation.**
|
|
15
|
+
- Publish refusals in Vietnamese, including accented/uppercase `chưa`, `không` and `ĐỪNG`, no longer create publication obligations. Real publication requests still require registry evidence and release-tag consistency.
|
|
16
|
+
- Conditional cron cancellation verifies the requested absence using a fresh native observation without inventing successful deletion. Existing in-flight contracts reconcile only the same conditional absence goal; mixed code tasks retain write/verification requirements.
|
|
17
|
+
- Blockers are task-scoped in Stop and unfinished-work detection. Current blockers end explicitly BLOCKED, never COMPLETE; previous-task blockers cannot discharge new-task verification.
|
|
18
|
+
- Preserve the established bypass startup template and explicit user permission choices. Instructions distinguish UKit workflow PLAN from host permission mode; these are preventive boundaries, not a runtime mode lock.
|
|
19
|
+
- **The reported Claude Code extension spontaneous permission-mode switch remains OPEN and is not claimed fixed.** The two VS Code User startup settings configured on the owner's machine are local settings, not distributed by this npm package.
|
|
20
|
+
- Includes the previously unreleased manual-only `/ukit:ci-check` tooling above; this release does not dispatch GitHub Actions.
|
|
21
|
+
|
|
22
|
+
## 3.4.43 - 2026-10-13
|
|
23
|
+
|
|
24
|
+
**Completion Gate: an ordered `git tag` is discharged by the tag itself, so the live release Stop is accepted.**
|
|
25
|
+
- Live root cause (3.4.42 still blocked this session): the prompt ordered a tag, so the git-integrity goal required target `tag`. A real tag created inside a compound command (`... && git tag ...; echo $?`) is a masked mutation and records as failed, and the independent remote-parity recovery only accepted `commit`/`push` targets, so `operation-verification` stayed missing although the tag, remote and registry were all correct.
|
|
26
|
+
- Fix: the gate-time git authority also lists the tags that dereference to live HEAD (`git tag --points-at HEAD`, peels annotated tags). When `tag` is a required target, a tag at HEAD plus the existing remote-parity proof (remote sha == HEAD on the tracking remote, clean tree, observed after every mutation attempt) recovers it. No tag at HEAD, a diverged remote, a dirty tree or a later mutation still blocks.
|
|
27
|
+
- Regressions in `tests/hooks/stopHookRelease.test.js`: masked tag with an annotated tag at HEAD -> ACCEPT (fails without the fix); no tag at HEAD -> BLOCK.
|
|
28
|
+
|
|
12
29
|
## 3.4.42 - 2026-10-13
|
|
13
30
|
|
|
14
31
|
**Completion Gate: `cd <repo>; <command>` is a checked command, so a finished release is recognized by the live Stop hook.**
|
|
@@ -407,6 +407,17 @@ entries:
|
|
|
407
407
|
load_policy: never
|
|
408
408
|
validation: [none]
|
|
409
409
|
archive_policy: immutable
|
|
410
|
+
- id: docs-analysis-claude-permission-mode
|
|
411
|
+
path: docs/analysis/CLAUDE_PERMISSION_MODE.md
|
|
412
|
+
class: canonical
|
|
413
|
+
audience: [maintainer]
|
|
414
|
+
owner: product
|
|
415
|
+
source_of_truth: docs/analysis/CLAUDE_PERMISSION_MODE.md
|
|
416
|
+
merge_strategy: none
|
|
417
|
+
load_policy: on-demand
|
|
418
|
+
validation: [manual]
|
|
419
|
+
archive_policy: owner-decides
|
|
420
|
+
notes: bounded investigation evidence; extension transition remains OPEN
|
|
410
421
|
- id: docs-analysis-subagent-cost-audit
|
|
411
422
|
path: docs/analysis/SUBAGENT-COST-AUDIT.md
|
|
412
423
|
class: archive
|
|
@@ -120,7 +120,10 @@ rules:
|
|
|
120
120
|
Under orchestration.permissionMode: unattended there is no human in the
|
|
121
121
|
loop — the loop is UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX →
|
|
122
122
|
REVERIFY → COMPLETE, and a turn ends only on COMPLETE evidence, a real
|
|
123
|
-
external blocker, or a deny decision.
|
|
123
|
+
external blocker, or a deny decision. Workflow phases do not authorize
|
|
124
|
+
changing the user-owned host permission mode: internal PLAN is not host
|
|
125
|
+
Plan Mode. Honor managed policy, newer user choices, and parent/subagent
|
|
126
|
+
and session boundaries.
|
|
124
127
|
marker_in: [template_project/CLAUDE.md, template_project/AGENTS.md, CLAUDE.md, AGENTS.md]
|
|
125
128
|
anchor: 'no human in the loop'
|
|
126
129
|
- id: UNATTENDED-02
|
package/package.json
CHANGED
|
@@ -1121,13 +1121,16 @@ export function classifyTaskSemantics({ promptText = '', targetFile = '' } = {})
|
|
|
1121
1121
|
&& /\b(?:check|status|report|show|summari[sz]e|result|results|kiem tra|bao cao|trang thai|ket qua)\b/.test(text);
|
|
1122
1122
|
const readOnly = (/\b(?:audit|inspect|review|analyze|analyse|explain|read|kiem tra|phan tich)\b/.test(text) || statusReport)
|
|
1123
1123
|
&& !sourceOrder && !codeChangeVerb && !operation && !config;
|
|
1124
|
-
const taskKind = operation ? (sourceOrder ||
|
|
1124
|
+
const taskKind = operation ? (sourceOrder || codeChangeVerb ? 'MIXED' : 'OPERATIONAL')
|
|
1125
1125
|
: config ? 'CONFIGURATION' : readOnly ? 'READ_ONLY' : 'CODE_IMPLEMENTATION';
|
|
1126
1126
|
const id = text.match(/\b[a-f0-9]{8}(?:[a-f0-9-]{0,28})\b/)?.[0] || null;
|
|
1127
1127
|
return { taskKind, ...(taskKind === 'READ_ONLY' && statusReport ? { statusReport: true } : {}), ...(config ? { configurationScope: repository ? 'repository' : 'external' } : {}),
|
|
1128
1128
|
...(operation && nativeDomain !== 'scheduled-job' ? { operationGoal: nativeOperationGoal(nativeDomain, actionable, text) } : {}),
|
|
1129
1129
|
...(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',
|
|
1130
|
-
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text)
|
|
1130
|
+
completionSemantics: /\b(?:ensure|dam bao)\b/.test(text)
|
|
1131
|
+
|| actionable.some((clause) => /\b(?:delete|remove|cancel|xoa|huy)\b/.test(clause)
|
|
1132
|
+
&& /\b(?:if\s+(?:it\s+)?(?:still\s+)?exists|if\s+present|neu\s+(?:van\s+|con\s+)?ton tai)\b/.test(clause))
|
|
1133
|
+
? 'ensure-state' : 'execute-and-verify' } } : {}) };
|
|
1131
1134
|
}
|
|
1132
1135
|
|
|
1133
1136
|
export function applyTaskSemantics(contract, semantic) {
|
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { withAsyncLock, LOCK_MAX_SLICE_MS } from './async-lock.mjs';
|
|
10
10
|
import { cronOperationReceipt, bashOperationReceipt, releaseOperationReceipt } from './cron-operation-adapter.mjs';
|
|
11
|
-
import { operationCompletionPolicy, foldOperationEvidence, compactOperation } from './operation-evidence.mjs';
|
|
11
|
+
import { operationCompletionPolicy, foldOperationEvidence, compactOperation, conditionalAbsenceGoal } from './operation-evidence.mjs';
|
|
12
12
|
|
|
13
13
|
// Hook-context self-deadline (2.4.1 orphan-leak class): armed ONLY on direct
|
|
14
14
|
// CLI invocation (see isDirectRun below) — never at import time. An import-time
|
|
@@ -348,6 +348,7 @@ function explicitPromptKey(routeState = {}) {
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
export function hasUnfinishedCompletion(state = {}, ledger = {}, { cwd } = {}) {
|
|
351
|
+
ledger = effectiveRequestLedger(state, ledger);
|
|
351
352
|
if (state?.routeSummary?.executionContract?.taskKind) {
|
|
352
353
|
const result = evaluateCompletion({ state, ledger, cwd });
|
|
353
354
|
return result.complete !== true && (result.missingEvidence || []).length > 0 && !ledger.blocker;
|
|
@@ -3268,8 +3269,18 @@ function scopedContract(state, ledger) {
|
|
|
3268
3269
|
const contract = state?.routeSummary?.executionContract;
|
|
3269
3270
|
if (!contract?.operationGoal) return contract;
|
|
3270
3271
|
const session = state?.sessionId || ledger?.sessionId;
|
|
3271
|
-
|
|
3272
|
-
|
|
3272
|
+
let goal = contract.operationGoal;
|
|
3273
|
+
// In-flight contracts retain their original semantics across tool reroutes. Reconcile
|
|
3274
|
+
// the conditional-absence correction only for the same native goal; never change
|
|
3275
|
+
// targets or discard the contract's existing code/verification obligations.
|
|
3276
|
+
if (goal.domain === 'scheduled-job' && goal.desiredState === 'absent'
|
|
3277
|
+
&& goal.completionSemantics === 'execute-and-verify') {
|
|
3278
|
+
if (conditionalAbsenceGoal(state?.routingContext?.lastExplicitUserPromptText, goal.target)) {
|
|
3279
|
+
goal = { ...goal, completionSemantics: 'ensure-state' };
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return { ...contract, operationGoal: { ...goal,
|
|
3283
|
+
scope: goal.scope || (session ? `session:${session}` : null) } };
|
|
3273
3284
|
}
|
|
3274
3285
|
|
|
3275
3286
|
// Gate-time local git truth for the remote-parity proof (cwd-bound, so it lives here, not in the pure evidence module).
|
|
@@ -3280,7 +3291,10 @@ function gitAuthority(cwd) {
|
|
|
3280
3291
|
if (!head || !branch) return null;
|
|
3281
3292
|
const remote = gitOutput(['config', `branch.${branch}.remote`], cwd);
|
|
3282
3293
|
const dirty = gitOutput(['status', '--porcelain'], cwd);
|
|
3283
|
-
|
|
3294
|
+
// Tags that dereference (`--points-at` peels annotated tags) to live HEAD: a masked `git tag` is recovered by
|
|
3295
|
+
// this independent local state, exactly like a masked push is recovered by remote parity.
|
|
3296
|
+
const tagsAtHead = (gitOutput(['tag', '--points-at', 'HEAD'], cwd) || '').split('\n').map((t) => t.trim()).filter(Boolean).slice(0, 16);
|
|
3297
|
+
return { head, branch, remote, tagsAtHead, clean: dirty === null && gitOutput(['rev-parse', '--git-dir'], cwd) !== null };
|
|
3284
3298
|
}
|
|
3285
3299
|
function withGitAuthority(ledger, cwd) {
|
|
3286
3300
|
const gitAuth = ledger && Object.keys(ledger).length ? gitAuthority(cwd) : null;
|
|
@@ -3305,10 +3319,11 @@ function effectiveRequestLedger(state = {}, ledger = {}) {
|
|
|
3305
3319
|
// stays missing. A registry that already serves the version counts as published, so a finished release is
|
|
3306
3320
|
// never re-published just to satisfy the gate.
|
|
3307
3321
|
function promptOrdersNpmPublish(state = {}) {
|
|
3308
|
-
const text = String(state?.routingContext?.lastExplicitUserPromptText || '')
|
|
3322
|
+
const text = String(state?.routingContext?.lastExplicitUserPromptText || '')
|
|
3323
|
+
.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[đĐ]/g, 'd').toLowerCase();
|
|
3309
3324
|
if (!/\bnpm\s+publish\b/.test(text)) return false;
|
|
3310
3325
|
return text.split(/[\n;.,]|\bthen\b|\band\b/).some((clause) => /\bnpm\s+publish\b/.test(clause)
|
|
3311
|
-
&& !/\b(?:do not|don't|never|without|not yet|dry-run|khong|dung)\b|--dry-run/.test(clause));
|
|
3326
|
+
&& !/\b(?:do not|don't|never|without|not yet|dry-run|khong|dung|chua)\b|--dry-run/.test(clause));
|
|
3312
3327
|
}
|
|
3313
3328
|
|
|
3314
3329
|
function releasePublicationMissing(state, ledger, cwd) {
|
|
@@ -3335,17 +3350,26 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3335
3350
|
const vibecode = routeSummary.autonomyLevel === 'vibecode';
|
|
3336
3351
|
const mode = routeSummary.executionMode || routeSummary.approachSelector?.executionMode || null;
|
|
3337
3352
|
const evidence = requiredEvidence(state);
|
|
3338
|
-
|
|
3353
|
+
// Request-scoped blocker: the stored session ledger persists until the new request's first
|
|
3354
|
+
// receipt re-keys it, so the RAW `ledger.blocker` can still be the PREVIOUS task's escape.
|
|
3355
|
+
// Scope the check to the effective request ledger — a stale blocker must never release the
|
|
3356
|
+
// new task's gate (its verification debt stays enforced below) — while the current task's
|
|
3357
|
+
// own blocker still ends the run truthfully: complete:false + outcome:'blocked', never a
|
|
3358
|
+
// fabricated complete and never another continuation loop.
|
|
3359
|
+
const requestLedger = effectiveRequestLedger(state, ledger);
|
|
3360
|
+
if (requestLedger?.blocker) {
|
|
3339
3361
|
// A blocker minted by the gate itself (verification loop) is the run's exit path — it
|
|
3340
3362
|
// must be user-visible or the release is indistinguishable from a stall. A blocker
|
|
3341
3363
|
// minted by a permission decision is already named in the model's own reply and stays
|
|
3342
3364
|
// silent here.
|
|
3343
|
-
if (
|
|
3365
|
+
if (requestLedger.blocker.visible === true) {
|
|
3344
3366
|
return {
|
|
3345
3367
|
continue: false,
|
|
3346
3368
|
notify: true,
|
|
3369
|
+
complete: false,
|
|
3370
|
+
outcome: 'blocked',
|
|
3347
3371
|
missingEvidence: [],
|
|
3348
|
-
reason: `UKit stopped automatic recovery: ${
|
|
3372
|
+
reason: `UKit stopped automatic recovery: ${requestLedger.blocker.detail || 'a recorded blocker ended this run.'}`,
|
|
3349
3373
|
};
|
|
3350
3374
|
}
|
|
3351
3375
|
// An invisible blocker (e.g. a permission decision the model already named in its own
|
|
@@ -3354,8 +3378,10 @@ function evaluateCompletionContract({ state = {}, ledger = {}, cwd, phase = null
|
|
|
3354
3378
|
return {
|
|
3355
3379
|
continue: false,
|
|
3356
3380
|
notify: true,
|
|
3381
|
+
complete: false,
|
|
3382
|
+
outcome: 'blocked',
|
|
3357
3383
|
missingEvidence: [],
|
|
3358
|
-
reason: `UKit stopped automatic recovery: ${
|
|
3384
|
+
reason: `UKit stopped automatic recovery: ${requestLedger.blocker.detail || 'a recorded blocker ended this run.'}`,
|
|
3359
3385
|
};
|
|
3360
3386
|
}
|
|
3361
3387
|
if (!state || !state.routeSummary) {
|
|
@@ -66,6 +66,17 @@ export function foldOperationEvidence(bank = {}, receipt, ledger) {
|
|
|
66
66
|
[key]: { ...prior, [op.role]: proof } };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function conditionalAbsenceGoal(promptText, target) {
|
|
70
|
+
if (typeof target !== 'string' || !/^[a-f0-9]{8}(?:[a-f0-9-]{0,28})$/.test(target)) return false;
|
|
71
|
+
const text = String(promptText || '').normalize('NFD')
|
|
72
|
+
.replace(/[\u0300-\u036f]/g, '').replace(/đ/g, 'd').toLowerCase();
|
|
73
|
+
return text.split(/[\n;.,]/).some((clause) => clause.split(/\s+/).some((word) => word === target)
|
|
74
|
+
&& /\b(?:cron|scheduled\s+job)\b/.test(clause)
|
|
75
|
+
&& /\b(?:delete|remove|cancel|xoa|huy)\b/.test(clause)
|
|
76
|
+
&& !/\b(?:do not|don't|never|khong|dung)\b/.test(clause)
|
|
77
|
+
&& /\b(?:if\s+(?:it\s+)?(?:still\s+)?exists|if\s+present|neu\s+(?:van\s+|con\s+)?ton tai)\b/.test(clause));
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
export function operationEvidenceStatus(goal, ledger = {}) {
|
|
70
81
|
if (!goal || typeof goal.domain !== 'string' || !goal.domain || typeof goal.desiredState !== 'string'
|
|
71
82
|
|| !goal.desiredState || (goal.target !== null && (typeof goal.target !== 'string' || !goal.target))
|
|
@@ -95,7 +106,7 @@ export function operationEvidenceStatus(goal, ledger = {}) {
|
|
|
95
106
|
// `ledger.gitAuthority` is the gate-time local truth; without it (or on any mismatch) nothing is proven.
|
|
96
107
|
const auth = ledger.gitAuthority;
|
|
97
108
|
if (goal.domain === 'git-integrity' && validRemoteSync(op.remoteSync) && auth && auth.clean === true
|
|
98
|
-
&& requiredTargets.every((t) => t === 'commit' || t === 'push')
|
|
109
|
+
&& requiredTargets.every((t) => t === 'commit' || t === 'push' || (t === 'tag' && Array.isArray(auth.tagsAtHead) && auth.tagsAtHead.length > 0))
|
|
99
110
|
&& attempts.every((r) => (Number.isSafeInteger(op.remoteSync.observedSequence) ? op.remoteSync.observedSequence : observation.sequence) > r.sequence)
|
|
100
111
|
&& op.remoteSync.sha === auth.head && op.remoteSync.ref === `refs/heads/${auth.branch}`
|
|
101
112
|
&& op.remoteSync.remote === auth.remote) return { complete: true, operationProof: 'remote-parity' };
|
|
@@ -104,7 +115,10 @@ export function operationEvidenceStatus(goal, ledger = {}) {
|
|
|
104
115
|
if (goal.domain === 'git-integrity' && validRemoteSync(op.remoteSync) && auth
|
|
105
116
|
&& (op.remoteSync.sha !== auth.head || op.remoteSync.ref !== `refs/heads/${auth.branch}`
|
|
106
117
|
|| (auth.remote && op.remoteSync.remote !== auth.remote))) return incomplete;
|
|
107
|
-
|
|
118
|
+
// Ensuring a state does not promise execution. A fresh native observation after
|
|
119
|
+
// every attempt proves the postcondition even when an already-absent delete failed.
|
|
120
|
+
if (goal.completionSemantics === 'ensure-state'
|
|
121
|
+
&& attempts.every((proof) => Number.isSafeInteger(proof.sequence) && observation.sequence > proof.sequence)) {
|
|
108
122
|
return { complete: true, operationProof: 'desired-state-only' };
|
|
109
123
|
}
|
|
110
124
|
if (!proofs.every((proof) => proof?.success && proof.toolUseId && observation.toolUseId
|
|
@@ -106,6 +106,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
|
|
|
106
106
|
## Unattended Completion Loop
|
|
107
107
|
<!-- RULE: UNATTENDED-01 -->
|
|
108
108
|
- `orchestration.permissionMode: unattended` = no human in the loop — UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny.
|
|
109
|
+
- UKit workflow phases are not host permissions: host permission mode remains user-owned (including Bypass, Default/Ask and Plan). PLAN is not a request to enter host Plan Mode; planning/review/handoff must not switch modes unless the user explicitly requests that transition. Respect host approval and managed policy; never restore an older mode over a newer user choice; subagent mode must not change the parent or another session.
|
|
109
110
|
<!-- RULE: UNATTENDED-02 -->
|
|
110
111
|
- `maxAttemptsPerFailure: 2`, `maxRecoveryStrategies: 2` distinct fix strategies before reassessing — retry the same edit, then fix the cause differently; never loop one strategy.
|
|
111
112
|
<!-- RULE: UNATTENDED-03 -->
|
|
@@ -106,6 +106,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
|
|
|
106
106
|
## Unattended Completion Loop
|
|
107
107
|
<!-- RULE: UNATTENDED-01 -->
|
|
108
108
|
- `orchestration.permissionMode: unattended` = no human in the loop — UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny.
|
|
109
|
+
- UKit workflow phases are not host permissions: host permission mode remains user-owned (including Bypass, Default/Ask and Plan). PLAN is not a request to enter host Plan Mode; planning/review/handoff must not switch modes unless the user explicitly requests that transition. Respect host approval and managed policy; never restore an older mode over a newer user choice; subagent mode must not change the parent or another session.
|
|
109
110
|
<!-- RULE: UNATTENDED-02 -->
|
|
110
111
|
- `maxAttemptsPerFailure: 2`, `maxRecoveryStrategies: 2` distinct fix strategies before reassessing — retry the same edit, then fix the cause differently; never loop one strategy.
|
|
111
112
|
<!-- RULE: UNATTENDED-03 -->
|
|
@@ -103,6 +103,7 @@ Chỉ active khi task đi qua `docs/AI_HANDOFF/` ("execute task TASK-xxx" hoặc
|
|
|
103
103
|
## Unattended Completion Loop
|
|
104
104
|
<!-- RULE: UNATTENDED-01 -->
|
|
105
105
|
- `orchestration.permissionMode: unattended` = no human in the loop — UNDERSTAND → PLAN → IMPLEMENT → VERIFY → FIX → REVERIFY → COMPLETE. A turn ends only on COMPLETE evidence, a real external blocker, or a deny.
|
|
106
|
+
- UKit workflow phases are not host permissions: host permission mode remains user-owned (including Bypass, Default/Ask and Plan). PLAN is not a request to enter host Plan Mode; planning/review/handoff must not switch modes unless the user explicitly requests that transition. Respect host approval and managed policy; never restore an older mode over a newer user choice; subagent mode must not change the parent or another session.
|
|
106
107
|
<!-- RULE: UNATTENDED-02 -->
|
|
107
108
|
- `maxAttemptsPerFailure: 2`, `maxRecoveryStrategies: 2` distinct fix strategies before reassessing — retry the same edit, then fix the cause differently; never loop one strategy.
|
|
108
109
|
<!-- RULE: UNATTENDED-03 -->
|