session-orchestrator 3.21.0 → 3.22.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +101 -0
- package/README.md +10 -10
- package/agents/memory-proposal-collector.md +6 -4
- package/commands/eli5.md +33 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/scope-collision-guard.md +3 -3
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +24 -92
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +7 -8
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/ci-status-banner.mjs +63 -57
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +147 -50
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +30 -0
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +6 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +32 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/discovery/SKILL.md +28 -24
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/session-end/SKILL.md +53 -20
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-start/SKILL.md +69 -35
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/wave-executor/wave-loop.md +27 -5
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
|
@@ -141,7 +141,8 @@ For every `SPIRAL` or `FAILED` agent surfaced in the walk above, ALSO append a c
|
|
|
141
141
|
```js
|
|
142
142
|
import { appendWhatNotToRetryOnDisk } from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
|
|
143
143
|
|
|
144
|
-
// `parsed` = parseStateMd(STATE.md); session
|
|
144
|
+
// `parsed` = parseStateMd(STATE.md); `session:` is an attribution/history label.
|
|
145
|
+
// It records this entry's provenance only and never authorizes lock ownership.
|
|
145
146
|
const sessionId = parsed.frontmatter.session ?? 'unknown-session';
|
|
146
147
|
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
147
148
|
|
|
@@ -646,7 +647,7 @@ import { planTailPhases } from '${PLUGIN_ROOT}/scripts/lib/session-end/phase-ski
|
|
|
646
647
|
const { plan, skippedReport } = await planTailPhases({
|
|
647
648
|
repoRoot: process.cwd(),
|
|
648
649
|
config, // parsed Session Config (from $CONFIG)
|
|
649
|
-
sessionId, // session.lock `session_id`
|
|
650
|
+
sessionId, // physical session.lock `session_id` only (or null), never STATE.md `session`
|
|
650
651
|
platform, // 'claude' | 'codex' | 'cursor'
|
|
651
652
|
});
|
|
652
653
|
// plan: Array<{ phase, run, reason, inputSource }>, already in ascending phase order.
|
|
@@ -733,20 +734,21 @@ After STATE.md is finalized with `status: completed` (Phase 3.4) and Recommendat
|
|
|
733
734
|
|
|
734
735
|
```javascript
|
|
735
736
|
import { release } from 'scripts/lib/session-lock.mjs';
|
|
736
|
-
// sessionId
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
const
|
|
737
|
+
// sessionId is the physical raw value established by session-start Phase 1.2
|
|
738
|
+
// and stored in .orchestrator/session.lock `session_id`. It is not STATE.md
|
|
739
|
+
// `session:` or `semantic_session_id`, both of which are attribution labels.
|
|
740
|
+
const rawSessionId = sessionId;
|
|
741
|
+
const result = release({ sessionId: rawSessionId, repoRoot: process.cwd() });
|
|
740
742
|
// result.ok is always true unless a filesystem error occurred.
|
|
741
743
|
// result.deleted === true → lock file removed successfully.
|
|
742
|
-
// result.deleted === false → lock was absent or
|
|
744
|
+
// result.deleted === false → lock was absent or had a different raw session_id.
|
|
743
745
|
```
|
|
744
746
|
|
|
745
|
-
If `result.deleted === false`, log `info: session-lock not released — already absent or session_id mismatch
|
|
747
|
+
If `result.deleted === false`, log `info: session-lock not released — already absent or raw session_id mismatch` and continue. An active lock whose raw id differs is ambiguous: do **not** retry release with an equal `semantic_session_id`, STATE.md `session`, or owner proof. Leave that live lock for its TTL/Reaper lifecycle.
|
|
746
748
|
|
|
747
749
|
If `result.ok === false` (rare filesystem error), log `⚠ session-lock: release failed — <result.reason>` and continue. Do NOT block the close for a lock-release failure — the TTL provides automatic expiry for the next session.
|
|
748
750
|
|
|
749
|
-
The lock is released here — AFTER all STATE.md writes are complete and BEFORE the commit is staged in Phase 4.1. This ordering ensures a clean handover: the lock file is absent from the working tree when the commit is assembled, so it is not accidentally staged.
|
|
751
|
+
The lock is released here — AFTER all STATE.md writes are complete and BEFORE the commit is staged in Phase 4.1. This ordering ensures a clean handover when the current raw owner releases it: the lock file is absent from the working tree when the commit is assembled, so it is not accidentally staged.
|
|
750
752
|
|
|
751
753
|
## Phase 4: Commit & Push
|
|
752
754
|
|
|
@@ -890,15 +892,24 @@ if (!promoted) {
|
|
|
890
892
|
When the worktree is dirty (uncommitted, untracked, OR unpushed), render this AUQ via the coordinator's `AskUserQuestion` tool. The AUQ is coordinator-only — per `.claude/rules/ask-via-tool.md` AUQ-004, dispatched agents cannot call AUQ. Calling `git worktree remove --force` without explicit operator confirmation would violate PSA-003 (destructive action safeguards) — the dirty state may contain another session's work-in-progress or unmerged commits.
|
|
891
893
|
|
|
892
894
|
```js
|
|
895
|
+
// What is actually at stake, shown beside the options via `preview` (AUQ-006):
|
|
896
|
+
// the operator must see WHICH changes he would lose before he authorises the delete.
|
|
897
|
+
// Capped at 10 lines so the preview never outgrows the option list next to it.
|
|
898
|
+
const dirtyDetail = execFileSync('git', ['-C', promoted.wtPath, 'status', '--short', '--branch'], { encoding: 'utf8' })
|
|
899
|
+
.trim()
|
|
900
|
+
.split('\n')
|
|
901
|
+
.slice(0, 10)
|
|
902
|
+
.join('\n');
|
|
903
|
+
|
|
893
904
|
AskUserQuestion({
|
|
894
905
|
questions: [{
|
|
895
906
|
question: `Auto-promoted worktree at ${promoted.wtPath} has uncommitted/untracked/unpushed changes. How should I proceed?`,
|
|
896
|
-
header: "Worktree
|
|
907
|
+
header: "Worktree",
|
|
897
908
|
multiSelect: false,
|
|
898
909
|
options: [
|
|
899
|
-
{ label: "Behalten (Recommended)", description: "
|
|
900
|
-
{ label: "Löschen", description: "I confirm the changes are handled or expendable. Run 'git worktree remove --force' on the worktree." },
|
|
901
|
-
{ label: "Manuell", description: "Exit /close. I will inspect the worktree before re-running /close." },
|
|
910
|
+
{ label: "Behalten (Recommended)", description: "Keeps the worktree exactly as it is — nothing is deleted, and you can still remove it by hand later.", preview: `Stays on disk:\n${dirtyDetail}` },
|
|
911
|
+
{ label: "Löschen", description: "I confirm the changes are handled or expendable. Run 'git worktree remove --force' on the worktree.", preview: `Deleted with the worktree:\n${dirtyDetail}` },
|
|
912
|
+
{ label: "Manuell", description: "Exit /close. I will inspect the worktree before re-running /close.", preview: `You would inspect this first:\n${dirtyDetail}` },
|
|
902
913
|
],
|
|
903
914
|
}],
|
|
904
915
|
});
|
|
@@ -907,8 +918,8 @@ AskUserQuestion({
|
|
|
907
918
|
**Codex CLI / Cursor IDE fallback** (numbered Markdown list):
|
|
908
919
|
|
|
909
920
|
```
|
|
910
|
-
Worktree cleanup options:
|
|
911
|
-
1. **Behalten (Recommended)** —
|
|
921
|
+
Worktree cleanup options (the changes at stake are the `git status --short --branch` lines printed above):
|
|
922
|
+
1. **Behalten (Recommended)** — Keeps the worktree exactly as it is; nothing is deleted, and you can still remove it by hand later.
|
|
912
923
|
2. **Löschen** — I confirm the changes are handled or expendable. Run 'git worktree remove --force'.
|
|
913
924
|
3. **Manuell** — Exit /close. I will inspect the worktree before re-running /close.
|
|
914
925
|
Reply with the number of your choice.
|
|
@@ -1026,16 +1037,38 @@ if (sweep) {
|
|
|
1026
1037
|
**Ordering (load-bearing):** run this as the LAST issue-creating action of Phase 5 — after step 3, after "Discovery Issue Creation", after step 4 — and re-read the counter file at that moment. Those steps can themselves push new entries into `overflow[]`; draining early would leave them unfiled.
|
|
1027
1038
|
|
|
1028
1039
|
```js
|
|
1029
|
-
import {
|
|
1030
|
-
|
|
1040
|
+
import { readFileSync } from 'node:fs';
|
|
1041
|
+
import {
|
|
1042
|
+
readBudgetState,
|
|
1043
|
+
budgetStatePath,
|
|
1044
|
+
resolveIssueBudgetSessionId,
|
|
1045
|
+
} from '${PLUGIN_ROOT}/scripts/lib/issue-budget.mjs';
|
|
1046
|
+
|
|
1047
|
+
// `sessionId` is the physical raw lock/registry identity from session-start.
|
|
1048
|
+
const rawSessionId = sessionId;
|
|
1049
|
+
let currentSession = null;
|
|
1050
|
+
try {
|
|
1051
|
+
currentSession = JSON.parse(
|
|
1052
|
+
readFileSync(`${repoRoot}/.orchestrator/current-session.json`, 'utf8'),
|
|
1053
|
+
);
|
|
1054
|
+
} catch { /* no verified semantic accounting bridge */ }
|
|
1055
|
+
const accountingSessionId = resolveIssueBudgetSessionId(rawSessionId, currentSession);
|
|
1056
|
+
const state = readBudgetState(repoRoot, accountingSessionId);
|
|
1057
|
+
// { sessionId, count, exempt, overflow: [...] }
|
|
1031
1058
|
```
|
|
1032
1059
|
|
|
1060
|
+
`accountingSessionId` may be semantic only after
|
|
1061
|
+
`currentSession.session_id === rawSessionId`; this is budget accounting, not
|
|
1062
|
+
lock/registry ownership. When that proof is absent it remains the raw id.
|
|
1063
|
+
A host rotation that changes both raw and semantic values has no guaranteed
|
|
1064
|
+
budget continuity.
|
|
1065
|
+
|
|
1033
1066
|
- **`issue-budget.overflow: collect-issue` (default)** — create exactly ONE issue:
|
|
1034
|
-
- Title: `[Backlog-Sammel] <
|
|
1067
|
+
- Title: `[Backlog-Sammel] <accountingSessionId>, <N> zurückgestellte Punkte`
|
|
1035
1068
|
- Labels: `type::backlog`, `priority::low`
|
|
1036
1069
|
- Body: a Markdown checklist with one `- [ ]` line per `overflow[]` entry (`title` when present, otherwise the truncated `command`, plus its `at` timestamp).
|
|
1037
1070
|
- This collector issue is itself EXEMPT from the cap (`[Backlog-Sammel]` is in the exemption list in `scripts/lib/issue-budget.mjs`), so it always lands even at count == max.
|
|
1038
|
-
- **`issue-budget.overflow: vault-note`** — create NO issue. Write one Markdown file `vault/00-inbox/<
|
|
1071
|
+
- **`issue-budget.overflow: vault-note`** — create NO issue. Write one Markdown file `vault/00-inbox/<accountingSessionId>-backlog-sammel.md` (path relative to `vault-integration.vault-dir`) with valid vault frontmatter and the same checklist body.
|
|
1039
1072
|
- After the artefact exists, reset `overflow` to `[]` in the counter file and record the collector issue ID / note path in the Phase 6 Final Report under `### Zurückgestellt (issue-budget)`.
|
|
1040
1073
|
- **Never exempt-by-accident:** the cap never applied to `priority::critical`, the carryover class (`[Carryover]`, SPIRAL/FAILED, `type::carryover`), or `broken-window` closure issues, so nothing on the Phase 1.65 carry-list can ever appear in `overflow[]`. The promises at Phase 1.8 ("SPIRAL / FAILED agent carryover … non-deselectable") and the Critical Rule "ALWAYS create issues for unfinished PLANNED work" stay intact by construction.
|
|
1041
1074
|
- Fail-open: a missing or malformed counter file means "no overflow" — log a WARN and continue the close.
|
|
@@ -1134,7 +1167,7 @@ Present to the user:
|
|
|
1134
1167
|
| `phase-3-7a-recommendations.md` § 3.7b | Phase 3.7b full procedural body — `withDurableCommit` invocation for `sessions.jsonl` + `STATE.md` (#490 AC2), `enabled:false` local no-op, autopilot.jsonl exclusion note |
|
|
1135
1168
|
| (inline) Phase 3.7c | Vault Board → Closed (#674) — `mirrorBoard({ explicitStatus: 'closed' })` transitions this repo's board row to `closed`; gated on `vault-integration.enabled`, generator-marked + idempotent, non-blocking, ordered after 3.7b and before 3.7d/3.4/3.8 |
|
|
1136
1169
|
| (inline) Phase 3.7d | Session-Eval (opt-in — #803) — `node scripts/eval-session.mjs --json` scores the just-closed session; gated on `eval.enabled` + `eval.mode != off` (parsed by `scripts/lib/config/eval.mjs`), optional `eval-judge` dispatch + `writeEvalReport`, advisory/never-blocks-close, ordered after 3.7 (record must exist) and before 3.4/Phase 4 (record committed with the session). Full flow in `skills/eval/SKILL.md` |
|
|
1137
|
-
| (inline) Phase 3.8 | Session Lock Release — `release()`
|
|
1170
|
+
| (inline) Phase 3.8 | Session Lock Release — `release()` uses the physical raw `session_id`; raw mismatch/absent is non-fatal but never repaired with semantic labels or proof (live ambiguity remains for TTL/Reaper); fs-errors are non-fatal; runs after STATE.md writes and before Phase 4 commit staging |
|
|
1138
1171
|
|
|
1139
1172
|
## Anti-Patterns
|
|
1140
1173
|
|
|
@@ -81,7 +81,25 @@ The proposals queue is populated mid-session by wave-executor agents calling `no
|
|
|
81
81
|
}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Then iterate `batches` and emit one `AskUserQuestion` per batch
|
|
84
|
+
Then iterate `batches` and emit one `AskUserQuestion` per batch. The verbatim template is `agents/memory-proposal-collector.md` § AUQ Question Template — keep the two in step:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
AskUserQuestion({
|
|
88
|
+
questions: [{
|
|
89
|
+
header: "Memory",
|
|
90
|
+
question: "Batch <N> of <M> — which of these learnings should be stored permanently?",
|
|
91
|
+
options: [
|
|
92
|
+
// one entry per proposal in this batch (max 4)
|
|
93
|
+
// label + description formats are LOCKED by D3 — see that file, do not restate them here
|
|
94
|
+
{ label: "[type ] | subject(40) | conf=X.XX", description: "evidence: <first 60 chars of insight>" },
|
|
95
|
+
...
|
|
96
|
+
],
|
|
97
|
+
multiSelect: true
|
|
98
|
+
}]
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The batch counter moved out of `header` and into the question because `header` is cut off after 12 characters — `Memory — Confirm Proposals (Batch N of M)` reached the operator as `Memory — Con`.
|
|
85
103
|
|
|
86
104
|
5. After all batches answered, partition the queue into `approved` (any option selected across all batches) and `rejected` (all unselected).
|
|
87
105
|
|
|
@@ -337,7 +355,24 @@ After the auto-dialectic nudge decision is made (Phase 3.6.7), and when the reco
|
|
|
337
355
|
}
|
|
338
356
|
```
|
|
339
357
|
|
|
340
|
-
Iterate `batches` and emit one `AskUserQuestion` per batch
|
|
358
|
+
Iterate `batches` and emit one `AskUserQuestion` per batch:
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
AskUserQuestion({
|
|
362
|
+
questions: [{
|
|
363
|
+
header: "Regeln",
|
|
364
|
+
question: "Batch <N> of <M> — which rule proposals should be written into .claude/rules/?",
|
|
365
|
+
options: [
|
|
366
|
+
// one entry per proposal in this batch (max 4)
|
|
367
|
+
{ label: "<slug-40>", description: "Confidence <confidence>. First 80 chars of the rendered rule text: <…>" },
|
|
368
|
+
...
|
|
369
|
+
],
|
|
370
|
+
multiSelect: true
|
|
371
|
+
}]
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The batch counter moved out of `header` and into the question because `header` is cut off after 12 characters — `Reconciliation — Confirm Rule Proposals (Batch N of M)` reached the operator as `Reconciliati`. The rendered `content` shown in the description is the rule prose that will land on disk.
|
|
341
376
|
|
|
342
377
|
6. After all batches are answered, partition proposals into `approved` (any option selected across all batches) and `rejected` (all unselected). Proposals the operator rejected join the engine's `rejected` array for archival.
|
|
343
378
|
|
|
@@ -42,7 +42,7 @@ This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's
|
|
|
42
42
|
**Outcome handling:**
|
|
43
43
|
- `PASS_THROUGH` → continue to Phase 1
|
|
44
44
|
- `EXCLUSIVE_BLOCKED` → exit Phase 0 cleanly per the AUQ outcome (`Warten` / `Andere Session beenden` / `Abbrechen` — all three return without initializing STATE.md)
|
|
45
|
-
- `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId()
|
|
45
|
+
- `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId() **for the worktree-name attribution label only**, `branch` from current HEAD, `repoRoot = process.cwd()`. It is not a lock/registry ownership key; the new worktree's Phase 1.2 obtains its own physical raw `session_id`. On success, exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit stderr WARN `parallel-aware: enterWorktree failed: <err>; falling back to Manuell` and proceed via the Manuell path.
|
|
46
46
|
- `PROMOTION_OFFER` with user picking "Manuell — in-place daneben" → append Deviation, continue to Phase 1
|
|
47
47
|
- `PROMOTION_OFFER` with user picking "Abbrechen" → exit cleanly
|
|
48
48
|
|
|
@@ -112,14 +112,14 @@ if (content && !isDispatcherAutonomyBlockPresent(content)) {
|
|
|
112
112
|
|
|
113
113
|
Acquire a distributed session-lock to detect parallel sessions in the same repo before initializing STATE.md. This prevents two concurrent Claude/Codex sessions from stomping each other's wave state and metrics writes.
|
|
114
114
|
|
|
115
|
-
**Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's `session_id` does not match the current session's id (a rare divergence — surface via AUQ before overwriting). The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
|
|
115
|
+
**Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's raw `session_id` does not exactly match the current session's raw id (a rare divergence — surface via AUQ before overwriting). A matching `semantic_session_id`, STATE.md `session`, or owner proof cannot repair that mismatch. The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
|
|
116
116
|
|
|
117
117
|
```javascript
|
|
118
118
|
import { acquire, forceAcquire } from 'scripts/lib/session-lock.mjs';
|
|
119
119
|
const result = acquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() });
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
Where `sessionId` is the
|
|
122
|
+
Where `sessionId` is the physical raw identity for this invocation: the native harness-provided raw id, or a generated UUID when no trustworthy raw id exists. It is the only value passed to `acquire()` and the only live lock/registry ownership key. `semanticSessionId` may be recorded separately as an attribution/history label and may populate STATE.md `session`; neither label is a substitute for `sessionId`. `sessionType` is the session mode (`housekeeping`, `feature`, or `deep`).
|
|
123
123
|
|
|
124
124
|
### Decision flow
|
|
125
125
|
|
|
@@ -131,21 +131,21 @@ Where `sessionId` is the session identifier derived from the session type and ti
|
|
|
131
131
|
```js
|
|
132
132
|
AskUserQuestion({
|
|
133
133
|
questions: [{
|
|
134
|
-
question: `Another session
|
|
135
|
-
header: "Session
|
|
134
|
+
question: `Another session holds the lock here — started ${ageHours}h ago, mode=${existingLock.mode}, host=${existingLock.host}, pid=${existingLock.pid}. Wait, or take the lock?`,
|
|
135
|
+
header: "Session lock",
|
|
136
136
|
multiSelect: false,
|
|
137
137
|
options: [
|
|
138
|
-
{ label: "Abort (Recommended)", description: "
|
|
139
|
-
{ label: "Force-take the lock", description: "Overwrites the active lock.
|
|
138
|
+
{ label: "Abort (Recommended)", description: "Stop here and let the other session finish, then start again. Nothing is written until it releases the lock, and two sessions sharing one wave state overwrite each other's metrics." },
|
|
139
|
+
{ label: "Force-take the lock", description: "Overwrites the active lock and starts anyway. Only when that session is certainly gone — otherwise both keep writing the same wave state and one of them loses everything." },
|
|
140
140
|
],
|
|
141
141
|
}],
|
|
142
142
|
});
|
|
143
143
|
```
|
|
144
144
|
- **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
|
|
145
145
|
```
|
|
146
|
-
|
|
147
|
-
1. Abort (Recommended) — let the other session finish.
|
|
148
|
-
2. Force-take the lock —
|
|
146
|
+
Another session holds the lock here — started <ageHours>h ago, mode=<mode>, host=<host>, pid=<pid>. Wait, or take the lock?
|
|
147
|
+
1. Abort (Recommended) — stop here and let the other session finish, then start again; nothing is written until it releases the lock.
|
|
148
|
+
2. Force-take the lock — overwrites the active lock. Only when that session is certainly gone, otherwise both keep writing the same wave state and one loses everything.
|
|
149
149
|
Reply with the number of your choice.
|
|
150
150
|
```
|
|
151
151
|
- On **Abort**: exit session-start cleanly with a brief stderr note (`session-lock: aborted — active lock held by session_id=<id>`). Do NOT initialize STATE.md.
|
|
@@ -158,21 +158,21 @@ Where `sessionId` is the session identifier derived from the session type and ti
|
|
|
158
158
|
```js
|
|
159
159
|
AskUserQuestion({
|
|
160
160
|
questions: [{
|
|
161
|
-
question: `
|
|
162
|
-
header: "Stale
|
|
161
|
+
question: `A stale session lock is in the way — started ${ageHours}h ago, its ttl=${existingLock.ttl_hours}h has expired, and pid=${existingLock.pid} on host=${existingLock.host} is ${reason === 'stale-pid-dead' ? 'confirmed dead' : 'still running or status unknown'}. Reclaim it?`,
|
|
162
|
+
header: "Stale lock",
|
|
163
163
|
multiSelect: false,
|
|
164
164
|
options: [
|
|
165
|
-
{ label: "Reclaim (Recommended)", description: "
|
|
166
|
-
{ label: "Abort — investigate manually", description: "
|
|
165
|
+
{ label: "Reclaim (Recommended)", description: "Overwrites the stale lock and continues, because its time-to-live has run out. When that process is really dead, nothing of the old session is lost." },
|
|
166
|
+
{ label: "Abort — investigate manually", description: "Stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive." },
|
|
167
167
|
],
|
|
168
168
|
}],
|
|
169
169
|
});
|
|
170
170
|
```
|
|
171
171
|
- **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
|
|
172
172
|
```
|
|
173
|
-
|
|
174
|
-
1. Reclaim (Recommended) —
|
|
175
|
-
2. Abort —
|
|
173
|
+
A stale session lock is in the way — started <ageHours>h ago, ttl=<ttlHours>h expired, pid=<pid> on <host>. Reclaim it?
|
|
174
|
+
1. Reclaim (Recommended) — overwrites the stale lock and continues, because its time-to-live has run out and that process is no longer holding anything.
|
|
175
|
+
2. Abort — stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive.
|
|
176
176
|
Reply with the number of your choice.
|
|
177
177
|
```
|
|
178
178
|
- On **Reclaim**: call `forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() })`. After Phase 1.5 initializes STATE.md, append a deviation:
|
|
@@ -196,11 +196,14 @@ When `existingLock.host !== os.hostname()`, PID liveness cannot be checked (`pid
|
|
|
196
196
|
|
|
197
197
|
> Skip this phase if `persistence` config is `false`.
|
|
198
198
|
|
|
199
|
-
After Phase 1.2 acquires (or confirms) the lock,
|
|
199
|
+
After Phase 1.2 acquires (or confirms) the lock, use `findPeers(repoRoot, { mySessionId: callerSessionHint })` for the STATE.md peer guard. `callerSessionHint` is the original semantic attribution label when one exists, otherwise the raw `sessionId`: `findPeers` may translate the semantic hint for the discovered lock/registry surface only after the exact raw binding check in `parallel-aware-preamble.md`, while keeping the original hint for STATE.md. This catches the rare case where lock-based detection missed an active peer (e.g., the peer's `session.lock` was force-deleted by an out-of-band sweep but STATE.md is still `status: active`, OR the peer's registry write succeeded but the lock-bootstrap hook crashed before the lock landed).
|
|
200
200
|
|
|
201
201
|
```javascript
|
|
202
202
|
import { findPeers } from '$PLUGIN_ROOT/scripts/lib/peer-discovery.mjs';
|
|
203
|
-
|
|
203
|
+
// Keep the STATE.md comparison in its original attribution-label space.
|
|
204
|
+
// findPeers performs the guarded semantic→raw translation only for discovered peers.
|
|
205
|
+
const callerSessionHint = semanticSessionId ?? sessionId;
|
|
206
|
+
const { peers } = await findPeers(process.cwd(), { mySessionId: callerSessionHint });
|
|
204
207
|
const peer = peers.find((p) => p.source === 'state-md') ?? null;
|
|
205
208
|
// Phase 1.2.1 consumes only the 'state-md' subset (STATE.md surface only).
|
|
206
209
|
if (peer) {
|
|
@@ -367,29 +370,60 @@ If `snaps.length >= 1` → present the following choice:
|
|
|
367
370
|
|
|
368
371
|
**Claude Code (AskUserQuestion):**
|
|
369
372
|
|
|
373
|
+
Before asking, read what "Recover" would actually put back — the operator decides on that diff, not on the word:
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
import { execFileSync } from 'node:child_process';
|
|
377
|
+
|
|
378
|
+
// Read-only: `git stash show` prints a diffstat and never touches the working tree.
|
|
379
|
+
// Capped at 12 lines so the preview box stays shorter than the option list beside it.
|
|
380
|
+
const stat = execFileSync('git', ['stash', 'show', '--stat', snaps[0].sha], { encoding: 'utf8' })
|
|
381
|
+
.split('\n').slice(0, 12).join('\n');
|
|
382
|
+
const refs = snaps.map((s) => s.ref).join('\n');
|
|
383
|
+
```
|
|
384
|
+
|
|
370
385
|
```js
|
|
371
386
|
AskUserQuestion({
|
|
372
387
|
questions: [{
|
|
373
|
-
question:
|
|
388
|
+
question: `${snaps.length} snapshot(s) from the resumed session, newest ${humanAgeOf(snaps[0].createdAt)}. Recover, keep, discard?`,
|
|
374
389
|
header: "Snapshot",
|
|
375
390
|
multiSelect: false,
|
|
376
391
|
options: [
|
|
377
|
-
{
|
|
378
|
-
|
|
379
|
-
|
|
392
|
+
{
|
|
393
|
+
label: "Recover (Recommended)",
|
|
394
|
+
description: "Puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.",
|
|
395
|
+
preview: `These files come back:\n\n\`\`\`\n${stat}\n\`\`\``,
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
label: "Keep as backup",
|
|
399
|
+
description: "Nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.",
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
label: "Discard all",
|
|
403
|
+
description: "Deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.",
|
|
404
|
+
preview: `Deleted for good:\n\n\`\`\`\n${refs}\n\`\`\``,
|
|
405
|
+
},
|
|
380
406
|
],
|
|
381
407
|
}],
|
|
382
408
|
});
|
|
383
409
|
```
|
|
384
410
|
|
|
411
|
+
`preview` renders beside the option list and only works with `multiSelect: false`. It is used here because the answer decides which literal text lands in the working tree — "Recover" is a diff, "Discard all" is a list of refs that stop existing. "Keep as backup" carries none: keeping is exactly the state the operator already sees.
|
|
412
|
+
|
|
385
413
|
**Codex CLI / Cursor IDE fallback (numbered Markdown list):**
|
|
386
414
|
|
|
415
|
+
These harnesses have no preview box, so the same diffstat is printed inline — it is the only place the operator ever sees it:
|
|
416
|
+
|
|
387
417
|
```markdown
|
|
388
|
-
|
|
418
|
+
"Recover" would put these files back:
|
|
419
|
+
|
|
420
|
+
<git stash show --stat <snaps[0].sha>, capped at 12 lines>
|
|
421
|
+
|
|
422
|
+
<N> snapshot(s) from the resumed session, newest <age>. Recover, keep, discard?
|
|
389
423
|
|
|
390
|
-
1. **Recover (Recommended)** —
|
|
391
|
-
2. **Keep as backup** —
|
|
392
|
-
3. **Discard all** —
|
|
424
|
+
1. **Recover (Recommended)** — puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.
|
|
425
|
+
2. **Keep as backup** — nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.
|
|
426
|
+
3. **Discard all** — deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.
|
|
393
427
|
|
|
394
428
|
Reply with the number of your choice.
|
|
395
429
|
```
|
|
@@ -471,7 +505,7 @@ await sweepBoard({
|
|
|
471
505
|
|
|
472
506
|
This single call does three things:
|
|
473
507
|
|
|
474
|
-
1. **Sets THIS repo's board row to `in-progress`** with the current semantic-session-id, branch, mode, and heartbeat (read off this repo's `session.lock` v2 lease + the host-wide registry — both already written by Phase 1.2's `acquire()`).
|
|
508
|
+
1. **Sets THIS repo's board row to `in-progress`** with the current semantic-session-id **attribution label** (never a lock/registry ownership key), branch, mode, and heartbeat (read off this repo's `session.lock` v2 lease + the host-wide registry — both already written by Phase 1.2's `acquire()`).
|
|
475
509
|
2. **Re-derives THIS repo's status from its live lease**, so a stale lease left by a prior crashed session in this same repo renders as `force-closed` (heartbeat older than the v2 ttl, default 4h — `DEFAULT_TTL_HOURS` in `scripts/lib/session-lock.mjs`, evaluated via `isLockLive`) and is **never silently dropped** — its fields are read straight off the dead lock.
|
|
476
510
|
3. **Re-derives every OTHER busy repo's status host-wide** via `enumerateCandidates` — a dead lease in repo B renders `force-closed` on the board the next time ANY repo's session-start runs `sweepBoard`, closing the #676→#716 gap. `frei` (lock-less) repos are excluded from re-derivation to avoid board noise; their prior rows, and the prior rows of any repo `enumerateCandidates` did not surface, are preserved unchanged via the idempotent merge — never dropped.
|
|
477
511
|
|
|
@@ -1049,12 +1083,12 @@ if (!c.prompt) {
|
|
|
1049
1083
|
```js
|
|
1050
1084
|
AskUserQuestion({
|
|
1051
1085
|
questions: [{
|
|
1052
|
-
question: "Anonyme Usage-Telemetrie aktivieren? Strikt opt-in,
|
|
1053
|
-
header: "
|
|
1086
|
+
question: "Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, jederzeit abschaltbar; was genau gesendet wird: docs/telemetry.md",
|
|
1087
|
+
header: "Telemetrie",
|
|
1054
1088
|
multiSelect: false,
|
|
1055
1089
|
options: [
|
|
1056
|
-
{ label: "Ja, aktivieren", description: "
|
|
1057
|
-
{ label: "Nein", description: "
|
|
1090
|
+
{ label: "Ja, aktivieren", description: "Sendet anonyme Zähl- und Strukturdaten (welche Phase lief, Erfolg oder Abbruch), whitelist-projiziert: keine Pfade, keine Prompts, keine Repo-Namen." },
|
|
1091
|
+
{ label: "Nein", description: "Sendet nichts; die Frage kommt hier nicht wieder. Einschalten geht später mit `node scripts/telemetry.mjs` (das ist der Befehl dafür)." },
|
|
1058
1092
|
],
|
|
1059
1093
|
}],
|
|
1060
1094
|
});
|
|
@@ -1064,9 +1098,9 @@ AskUserQuestion({
|
|
|
1064
1098
|
|
|
1065
1099
|
- **Codex CLI / Cursor IDE fallback (numbered Markdown list — AUQ-004 exception 1):**
|
|
1066
1100
|
```
|
|
1067
|
-
Anonyme Usage-Telemetrie aktivieren? Strikt opt-in,
|
|
1068
|
-
1. Ja, aktivieren —
|
|
1069
|
-
2. Nein —
|
|
1101
|
+
Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, jederzeit abschaltbar; was genau gesendet wird: docs/telemetry.md
|
|
1102
|
+
1. Ja, aktivieren — sendet anonyme Zähl- und Strukturdaten (welche Phase lief, Erfolg oder Abbruch), whitelist-projiziert: keine Pfade, keine Prompts, keine Repo-Namen.
|
|
1103
|
+
2. Nein — sendet nichts; die Frage kommt hier nicht wieder. Einschalten geht später mit `node scripts/telemetry.mjs` (das ist der Befehl dafür).
|
|
1070
1104
|
Reply with the number of your choice. (No option is pre-recommended — the choice is yours.)
|
|
1071
1105
|
```
|
|
1072
1106
|
|
|
@@ -58,15 +58,15 @@ AskUserQuestion({
|
|
|
58
58
|
options: [
|
|
59
59
|
{
|
|
60
60
|
label: "Dev (Recommended)", // add "(Recommended)" to each detected audience
|
|
61
|
-
description: "Architektur-, Modul
|
|
61
|
+
description: "Architektur-, Modul-, Refactoring-Änderungen. Dann ändern sich CLAUDE.md bzw. AGENTS.md und `docs/dev/**, docs/adr/**` (Handbuch und Entscheidungen)."
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
label: "User",
|
|
65
|
-
description: "Öffentlich sichtbare Änderungen
|
|
65
|
+
description: "Öffentlich sichtbare Änderungen. Dann werden README.md und `docs/user/**, examples/**` (was Nutzer davon lesen) nachgezogen."
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
label: "Vault",
|
|
69
|
-
description: "Strategische oder Status-Änderungen
|
|
69
|
+
description: "Strategische oder Status-Änderungen. Dann wird `<vault>/01-projects/<slug>/context.md, decisions.md, people.md` (die Projektakte dazu) nachgezogen."
|
|
70
70
|
}
|
|
71
71
|
]
|
|
72
72
|
}]
|
|
@@ -76,13 +76,13 @@ AskUserQuestion({
|
|
|
76
76
|
**Codex CLI / Cursor IDE fallback (numbered Markdown list):**
|
|
77
77
|
|
|
78
78
|
```markdown
|
|
79
|
-
Welche Audiences berührt dieser Scope? (Mehrfachauswahl möglich)
|
|
80
|
-
|
|
81
79
|
Auto-detected: [dev] ← list detected audiences here, or "none" if empty intersection
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
Welche Audiences berührt dieser Scope? Mehrfachauswahl möglich.
|
|
82
|
+
|
|
83
|
+
1. **Dev (Recommended)** — Architektur-, Modul-, Refactoring-Änderungen. Dann ändern sich CLAUDE.md bzw. AGENTS.md und `docs/dev/**, docs/adr/**` (Handbuch und Entscheidungen).
|
|
84
|
+
2. **User** — öffentlich sichtbare Änderungen. Dann werden README.md und `docs/user/**, examples/**` (was Nutzer davon lesen) nachgezogen.
|
|
85
|
+
3. **Vault** — strategische oder Status-Änderungen. Dann wird `<vault>/01-projects/<slug>/context.md, decisions.md, people.md` (die Projektakte dazu) nachgezogen.
|
|
86
86
|
|
|
87
87
|
Enter one or more numbers (comma-separated), or press Enter to accept the recommended default.
|
|
88
88
|
```
|
|
@@ -16,31 +16,88 @@ const verdict = evaluate(snapshot, config['resource-thresholds'], {
|
|
|
16
16
|
});
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The `evaluate()` result has
|
|
20
|
-
- `verdict`: `green` | `warn` | `critical`
|
|
21
|
-
- `reasons`: array of human-readable explanations
|
|
19
|
+
The `evaluate()` result has four fields:
|
|
20
|
+
- `verdict`: `green` | `warn` | `critical` (the `degraded` tier is no longer produced)
|
|
21
|
+
- `reasons`: array of human-readable explanations, including `info:`-prefixed
|
|
22
|
+
lines for signals that were seen but deliberately not acted on
|
|
22
23
|
- `recommended_agents_per_wave_cap`: integer cap (0 = coordinator-direct) or null
|
|
24
|
+
- `signals`: `{ hard: string[], soft: string[] }` — which axes fired (#1089)
|
|
23
25
|
|
|
24
26
|
The third `options` argument is optional (HR-003/HR-004, baseline #60) — when `config['heavy-repo']` is `true`, the cap is forced to at most `config['agents-per-wave']` regardless of the live verdict (static preflight ceiling; more-restrictive-wins against whatever the resource signals already computed). Omitting `options` entirely preserves pre-#60 behaviour.
|
|
25
27
|
|
|
26
|
-
## Adaptive Rules (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
## Adaptive Rules (rebuilt in #1089 — see `.claude/rules/host-resources.md`)
|
|
29
|
+
|
|
30
|
+
The rule set is **signal precedence + the two-signal rule**, not a list of
|
|
31
|
+
independent thresholds ORed together. Measured 2026-08-21 over 1477
|
|
32
|
+
`orchestrator.session.started` events across 18 repos, the previous OR-of-three
|
|
33
|
+
produced warn-or-worse on **99.0%** of session starts — a warning that fires
|
|
34
|
+
almost always changes no decision except how fast it gets ignored.
|
|
35
|
+
|
|
36
|
+
**Memory — judge on the best signal present, never on a worse one:**
|
|
37
|
+
|
|
38
|
+
| Precedence | Signal | Hard (→ critical) | Soft |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| 1 | `memory_pressure_pct_free` (macOS) | `< 15%` | `< 30%` |
|
|
41
|
+
| 2 | `ram_available_gb` (macOS, vm_stat) | `< ram-free-critical-gb` | `< ram-free-min-gb` |
|
|
42
|
+
| 3 | `ram_free_gb` (`os.freemem`) | same | same |
|
|
43
|
+
|
|
44
|
+
Level 3 is reached only when neither better signal exists — i.e. on
|
|
45
|
+
Linux/Windows, where `os.freemem()` is accurate. On Darwin it reports `Pages
|
|
46
|
+
free` only (median **0.4 GB** across the corpus, on 24-128 GB hosts), so gating
|
|
47
|
+
on it there fired the *critical* threshold on 84.0% of starts.
|
|
48
|
+
|
|
49
|
+
**Other axes:**
|
|
50
|
+
|
|
51
|
+
| Signal | Threshold | Class |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| CPU, judged on **min(1m, 5m)** (#943) | above `cpu-load-max-pct` (default 90) | soft |
|
|
54
|
+
| Live peer **sessions** from the registry | ≥ `concurrent-sessions-warn` (default 5) | soft |
|
|
55
|
+
| Claude **processes** (fallback only, registry unreadable) | ≥ threshold × 6 | soft |
|
|
56
|
+
| Swap, **only while memory is unhealthy** | `> 3072 MB` hard / `> 1024 MB` soft | both |
|
|
57
|
+
| Zombie processes with a live peer/process context | ≥ 1 | **info** (reported, never counted — see HR-104) |
|
|
58
|
+
| SSH detected AND `ssh-no-docker: true` | — | info note |
|
|
59
|
+
|
|
60
|
+
**Verdict composition:**
|
|
61
|
+
|
|
62
|
+
- any **hard** signal → `critical`, recommend coordinator-direct (0 agents)
|
|
63
|
+
- **two or more independent soft** signals → `warn`, cap agents-per-wave at 2
|
|
64
|
+
- exactly **one soft** signal → `green`, reported in `reasons`, **no cap**
|
|
65
|
+
- none → `green`
|
|
66
|
+
|
|
67
|
+
`evaluate()` additionally returns `signals: { hard: [...], soft: [...] }` so a
|
|
68
|
+
caller can log which axes fired rather than re-deriving them from prose.
|
|
69
|
+
|
|
70
|
+
**Unit note (#1089):** `concurrent-sessions-warn` is denominated in SESSIONS. It
|
|
71
|
+
was compared against `claude_processes_count` until this rebuild — a measured 6x
|
|
72
|
+
unit error (median processes:sessions = 6.0 over 1461 paired samples) that made
|
|
73
|
+
the threshold fire on 93.6% of starts instead of 4.2%. `probe()` now supplies
|
|
74
|
+
`peer_sessions_count` from the session registry (`detectPeers()`, self excluded,
|
|
75
|
+
heartbeat-fresh); the rescaled process count is a fallback for hosts where the
|
|
76
|
+
registry is unreadable.
|
|
77
|
+
|
|
78
|
+
**CPU methodology (#943):** the gate/probe runs right after the coordinator's own
|
|
79
|
+
CPU-saturating quality-gate run by construction, so the 1-minute load average
|
|
80
|
+
systematically carries that decaying tail (observed: 96% → 75% within 36s).
|
|
81
|
+
`probe()` therefore also emits `cpu_load_5m` / `cpu_load_5m_pct`, and both
|
|
82
|
+
`evaluate()` and `evaluateWaveResourceGate()` judge CPU on **min(1m, 5m)**:
|
|
83
|
+
only-1m-high is reported as an informational "decaying transient" and is NOT
|
|
84
|
+
counted as a signal at all — so it cannot become the second signal that triggers
|
|
85
|
+
a cap. When `cpu_load_5m_pct` is `null` (Windows, zero-load), judging falls back
|
|
86
|
+
to `cpu_load_pct` alone.
|
|
37
87
|
|
|
38
88
|
## Presentation
|
|
39
89
|
|
|
40
90
|
Print a one-line Resource Health verdict immediately after Phase 4's output:
|
|
41
91
|
|
|
42
92
|
```
|
|
43
|
-
Resource Health: ⚠ warn —
|
|
93
|
+
Resource Health: ⚠ warn — two signals agree (cpu + concurrency); capping agents-per-wave at 2.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
On `green` with a reported signal, print the line but say plainly that nothing
|
|
97
|
+
was capped — a bare signal with no consequence reads as a suppressed warning:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Resource Health: ✓ green — 6884 MB swap present but memory_pressure healthy (53% free); no cap.
|
|
44
101
|
```
|
|
45
102
|
|
|
46
103
|
When `config['heavy-repo']` is `true` and the HR-004 preflight ceiling actually reduces `recommended_agents_per_wave_cap` below what the live verdict alone would have produced, print an additional banner line right after the verdict line:
|
|
@@ -49,10 +106,16 @@ When `config['heavy-repo']` is `true` and the HR-004 preflight ceiling actually
|
|
|
49
106
|
⚠ Heavy-repo mode active — agents-per-wave capped to 4 (Session Config heavy-repo: true)
|
|
50
107
|
```
|
|
51
108
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
109
|
+
**AUQ only on `critical`** (#1089). A `warn` applies its cap and reports it in
|
|
110
|
+
one line — it does not interrupt. Under the previous rule set warn-or-worse was
|
|
111
|
+
the verdict on 99.0% of session starts, so an AUQ here was an operator interrupt
|
|
112
|
+
on essentially every session, which `.claude/rules/ask-via-tool.md` AUQ-005
|
|
113
|
+
names outright ("an AUQ that blocks nothing"). `critical` means coordinator-direct
|
|
114
|
+
— zero agents — which genuinely changes the plan, so it earns the prompt:
|
|
115
|
+
|
|
116
|
+
1. **Proceed coordinator-direct** (0 agents) — Recommended
|
|
117
|
+
2. **Proceed as originally planned** (operator accepts the risk)
|
|
118
|
+
3. **Abort** (no wave planning runs; operator closes or investigates)
|
|
56
119
|
|
|
57
120
|
When SSH is detected and the session type is `deep`, auto-append this note to the plan handoff to session-plan (no user prompt needed):
|
|
58
121
|
> Host is SSH-attached — Docker-dependent wave steps should run on a local dev host.
|