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
|
@@ -18,6 +18,7 @@ import { promisify } from 'node:util';
|
|
|
18
18
|
import {
|
|
19
19
|
resolveRepoSpec,
|
|
20
20
|
resolveRepoHost,
|
|
21
|
+
resolveGitlabProjectTarget,
|
|
21
22
|
redactUrlCredentials,
|
|
22
23
|
detectVcsFamily,
|
|
23
24
|
isQueryFailure,
|
|
@@ -191,42 +192,19 @@ async function getHeadSha(repoRoot, deps = {}) {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
/**
|
|
194
|
-
*
|
|
195
|
+
* Run a host-pinned `glab api <path>` request and return parsed JSON.
|
|
195
196
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* @param {string} repoRoot
|
|
200
|
-
* @param {{ execFile?: Function, timeoutMs?: number, repoSpec?: string }} deps
|
|
201
|
-
* @returns {Promise<number>}
|
|
202
|
-
*/
|
|
203
|
-
async function getGlabProjectId(repoRoot, deps = {}) {
|
|
204
|
-
const args = ['repo', 'view', '--output', 'json'];
|
|
205
|
-
if (deps.repoSpec) args.push('-R', deps.repoSpec);
|
|
206
|
-
const result = await execWithTimeout(
|
|
207
|
-
'glab',
|
|
208
|
-
args,
|
|
209
|
-
{ cwd: repoRoot, timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS, execFile: deps.execFile },
|
|
210
|
-
);
|
|
211
|
-
const parsed = JSON.parse(result.stdout);
|
|
212
|
-
return parsed.id;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Run `glab api <path>` and return parsed JSON.
|
|
217
|
-
*
|
|
218
|
-
* #872: `glab api` has no repo/`-R` concept — it accepts only `--hostname`
|
|
219
|
-
* to pin which GitLab instance the request targets. Pinned via
|
|
220
|
-
* `deps.repoHost` when resolved.
|
|
197
|
+
* GitLab's API accepts neither a remote URL nor `-R`; callers provide the host
|
|
198
|
+
* proven by `resolveGitlabProjectTarget` so this helper cannot fall back to
|
|
199
|
+
* ambient `GITLAB_HOST` configuration.
|
|
221
200
|
*
|
|
222
201
|
* @param {string} apiPath
|
|
223
202
|
* @param {string} repoRoot
|
|
224
|
-
* @param {{ execFile?: Function, timeoutMs?: number, repoHost
|
|
203
|
+
* @param {{ execFile?: Function, timeoutMs?: number, repoHost: string }} deps
|
|
225
204
|
* @returns {Promise<unknown>}
|
|
226
205
|
*/
|
|
227
206
|
async function glabApi(apiPath, repoRoot, deps = {}) {
|
|
228
|
-
const args = ['api', apiPath];
|
|
229
|
-
if (deps.repoHost) args.push('--hostname', deps.repoHost);
|
|
207
|
+
const args = ['api', apiPath, '--hostname', deps.repoHost];
|
|
230
208
|
const result = await execWithTimeout(
|
|
231
209
|
'glab',
|
|
232
210
|
args,
|
|
@@ -276,17 +254,32 @@ function ageDaysFrom(isoDate, now) {
|
|
|
276
254
|
*
|
|
277
255
|
* @param {string} repoRoot
|
|
278
256
|
* @param {number} now
|
|
279
|
-
* @param {{
|
|
257
|
+
* @param {{
|
|
258
|
+
* execFile?: Function,
|
|
259
|
+
* timeoutMs?: number,
|
|
260
|
+
* gitlabProject?: { host: string, encodedProjectPath: string },
|
|
261
|
+
* }} deps
|
|
280
262
|
* @returns {Promise<object|null>}
|
|
281
263
|
*/
|
|
282
264
|
async function checkGitlab(repoRoot, now, deps = {}) {
|
|
283
|
-
const
|
|
284
|
-
|
|
265
|
+
const project = deps.gitlabProject;
|
|
266
|
+
if (
|
|
267
|
+
!project ||
|
|
268
|
+
typeof project.host !== 'string' ||
|
|
269
|
+
typeof project.encodedProjectPath !== 'string' ||
|
|
270
|
+
project.host === '' ||
|
|
271
|
+
project.encodedProjectPath === ''
|
|
272
|
+
) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
285
275
|
|
|
276
|
+
const currentSha = await getHeadSha(repoRoot, deps);
|
|
277
|
+
const apiDeps = { ...deps, repoHost: project.host };
|
|
278
|
+
const projectPath = `projects/${project.encodedProjectPath}`;
|
|
286
279
|
const pipelines = await glabApi(
|
|
287
|
-
|
|
280
|
+
`${projectPath}/pipelines?order_by=updated_at&sort=desc&per_page=15`,
|
|
288
281
|
repoRoot,
|
|
289
|
-
|
|
282
|
+
apiDeps,
|
|
290
283
|
);
|
|
291
284
|
|
|
292
285
|
if (!Array.isArray(pipelines)) return null;
|
|
@@ -316,9 +309,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
|
|
|
316
309
|
let allowFailureJobs;
|
|
317
310
|
try {
|
|
318
311
|
const jobs = await glabApi(
|
|
319
|
-
|
|
312
|
+
`${projectPath}/pipelines/${currentPipeline.id}/jobs`,
|
|
320
313
|
repoRoot,
|
|
321
|
-
|
|
314
|
+
apiDeps,
|
|
322
315
|
);
|
|
323
316
|
if (Array.isArray(jobs)) {
|
|
324
317
|
const softFailed = jobs
|
|
@@ -383,9 +376,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
|
|
|
383
376
|
let failingJobName;
|
|
384
377
|
try {
|
|
385
378
|
const jobs = await glabApi(
|
|
386
|
-
|
|
379
|
+
`${projectPath}/pipelines/${currentPipeline.id}/jobs`,
|
|
387
380
|
repoRoot,
|
|
388
|
-
|
|
381
|
+
apiDeps,
|
|
389
382
|
);
|
|
390
383
|
if (Array.isArray(jobs)) {
|
|
391
384
|
const failedJob = jobs.find((j) => j.status === 'failed');
|
|
@@ -433,9 +426,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
|
|
|
433
426
|
* `[HOST/]OWNER/REPO` shape `resolveRepoSpec({ vcs: 'github' })` returns is
|
|
434
427
|
* exactly the positional's documented input format.
|
|
435
428
|
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
429
|
+
* GitLab CI differs deliberately: it derives a host-pinned API target from
|
|
430
|
+
* the selected remote, while GitHub still needs this lookup because its API
|
|
431
|
+
* path requires `nameWithOwner`. Do not unify these call sites.
|
|
439
432
|
*
|
|
440
433
|
* `nameWithOwner` is NOT derivable from `deps.repoSpec`, so this lookup
|
|
441
434
|
* cannot be dropped: the spec carries a HOST prefix the `repos/<owner>/<repo>`
|
|
@@ -540,10 +533,11 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
540
533
|
* execFile?: Function,
|
|
541
534
|
* resolveRepoSpec?: (opts: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined,
|
|
542
535
|
* resolveRepoHost?: (opts: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined,
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
536
|
+
* resolveGitlabProjectTarget?: (opts: { repoRoot: string }) =>
|
|
537
|
+
* { host: string, encodedProjectPath: string }|undefined,
|
|
538
|
+
* }} deps Dependency-injection seam for testing. GitLab defaults to
|
|
539
|
+
* `resolveGitlabProjectTarget`, which proves host and project path from one
|
|
540
|
+
* sanitized remote; GitHub retains the #872 spec/host resolvers.
|
|
547
541
|
* @returns {Promise<null | {
|
|
548
542
|
* status: 'green'|'red'|'unknown',
|
|
549
543
|
* ok: boolean,
|
|
@@ -572,6 +566,7 @@ export async function checkCiStatus(opts = {}, deps = {}) {
|
|
|
572
566
|
|
|
573
567
|
const resolveRepoSpecDep = deps.resolveRepoSpec ?? resolveRepoSpec;
|
|
574
568
|
const resolveRepoHostDep = deps.resolveRepoHost ?? resolveRepoHost;
|
|
569
|
+
const resolveGitlabProjectTargetDep = deps.resolveGitlabProjectTarget ?? resolveGitlabProjectTarget;
|
|
575
570
|
|
|
576
571
|
const depsWithExec = { execFile: execFileDep, timeoutMs };
|
|
577
572
|
|
|
@@ -605,22 +600,33 @@ export async function checkCiStatus(opts = {}, deps = {}) {
|
|
|
605
600
|
vcs = detected.vcs;
|
|
606
601
|
}
|
|
607
602
|
|
|
608
|
-
// Step 1b
|
|
609
|
-
//
|
|
610
|
-
// GITLAB_HOST/
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
//
|
|
603
|
+
// Step 1b: GitLab's API target must be proven before its first glab spawn.
|
|
604
|
+
// A missing target is never permission to fall back to ambient
|
|
605
|
+
// GITLAB_HOST/repository configuration. GitHub retains its distinct
|
|
606
|
+
// repository lookup because its API path requires `nameWithOwner`.
|
|
607
|
+
//
|
|
608
|
+
// It is also not an ABSENCE: detectVcs just proved a GitLab remote exists,
|
|
609
|
+
// so `!gitlabProject` means "remote present, its form was rejected" — a
|
|
610
|
+
// QUERY FAILURE by the same rule the block above states. Returning null
|
|
611
|
+
// silently put this repo back in the pre-#1039 state where "CI green" and
|
|
612
|
+
// "could not ask" look identical. Reachable inputs measured 2026-08-21:
|
|
613
|
+
// `git://` scheme, a doubled slash in the path, a query string.
|
|
618
614
|
if (vcs === 'gitlab') {
|
|
619
|
-
|
|
615
|
+
const gitlabProject = resolveGitlabProjectTargetDep({ repoRoot });
|
|
616
|
+
if (!gitlabProject) {
|
|
617
|
+
console.warn(
|
|
618
|
+
'WARN ci-status-banner: a GitLab remote was detected but its host/project path ' +
|
|
619
|
+
'could not be derived, banner suppressed — CI state is UNKNOWN, not "green".',
|
|
620
|
+
);
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
return await checkGitlab(repoRoot, now, { ...depsWithExec, gitlabProject });
|
|
620
624
|
}
|
|
621
625
|
|
|
622
626
|
if (vcs === 'github') {
|
|
623
|
-
|
|
627
|
+
const repoSpec = resolveRepoSpecDep({ repoRoot, vcs });
|
|
628
|
+
const repoHost = resolveRepoHostDep({ repoRoot, vcs });
|
|
629
|
+
return await checkGithub(repoRoot, { ...depsWithExec, repoSpec, repoHost });
|
|
624
630
|
}
|
|
625
631
|
|
|
626
632
|
// Unknown VCS value — silent no-op.
|
|
@@ -43,13 +43,36 @@ import { hasBlockHeader } from './block-header.mjs';
|
|
|
43
43
|
const ALLOWED_AUTONOMY = ['off', 'advisory', 'autonomous-gated'];
|
|
44
44
|
const DEFAULT_CONFIDENCE_FLOOR = 0.5;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The display-only tail of an option LABEL, stripped before the enum match.
|
|
48
|
+
*
|
|
49
|
+
* The label and the stored value were the SAME string, which made the label
|
|
50
|
+
* unchangeable: both consumers (`skills/session-start/SKILL.md` Phase 1.1 and
|
|
51
|
+
* `skills/bootstrap/SKILL.md` Phase 3.5.1) hand the SELECTED LABEL straight to
|
|
52
|
+
* writeDispatcherAutonomyBlock(), and coerceAutonomy() falls back to 'off' on
|
|
53
|
+
* any value outside the enum. Putting AUQ-003's `(Recommended)` marker into the
|
|
54
|
+
* label would therefore have written `autonomy: off` for an operator who picked
|
|
55
|
+
* `advisory` — a silent downgrade, fail-closed and therefore invisible.
|
|
56
|
+
*
|
|
57
|
+
* Measured before this split (renderDispatcherAutonomyBlock, 2026-08-22):
|
|
58
|
+
* 'advisory' => autonomy: advisory
|
|
59
|
+
* 'Advisory (surface only)' => autonomy: off ← the downgrade
|
|
60
|
+
*
|
|
61
|
+
* Anchored at the end, so it can only ever strip a suffix. The enum values
|
|
62
|
+
* themselves are untouched and stay greppable.
|
|
63
|
+
*/
|
|
64
|
+
const RECOMMENDED_SUFFIX = /\s*\((?:Recommended|Empfohlen|Default)\)\s*$/u;
|
|
65
|
+
|
|
46
66
|
/**
|
|
47
67
|
* Coordinator-facing AUQ question definition for the one-time capture.
|
|
48
68
|
* Mirrors the shape of `getInterviewQuestions()` entries in owner-interview.mjs:
|
|
49
69
|
* { question, header, options: [{ label, description }], multiSelect }
|
|
50
70
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
71
|
+
* Each label BEGINS with its ALLOWED_AUTONOMY value; option 1 additionally
|
|
72
|
+
* carries the `(Recommended)` marker AUQ-003 puts in the label. `coerceAutonomy()`
|
|
73
|
+
* strips that marker, so the selected label still maps to the enum value written
|
|
74
|
+
* by writeDispatcherAutonomyBlock() — see the comment on RECOMMENDED_SUFFIX for
|
|
75
|
+
* why the two halves must not be the same string.
|
|
53
76
|
* Option 1 (`off`) is the recommended, fail-closed default.
|
|
54
77
|
*
|
|
55
78
|
* @returns {{ question: string, header: string, options: Array<{ label: string, description: string }>, multiSelect: boolean }}
|
|
@@ -57,23 +80,23 @@ const DEFAULT_CONFIDENCE_FLOOR = 0.5;
|
|
|
57
80
|
export function getDispatcherAutonomyQuestion() {
|
|
58
81
|
return {
|
|
59
82
|
question:
|
|
60
|
-
'
|
|
61
|
-
header: 'Dispatcher
|
|
83
|
+
'When /dispatcher looks across your repos for the next one to work on, how may it treat this repo?',
|
|
84
|
+
header: 'Dispatcher',
|
|
62
85
|
options: [
|
|
63
86
|
{
|
|
64
|
-
label: 'off',
|
|
87
|
+
label: 'off (Recommended)',
|
|
65
88
|
description:
|
|
66
|
-
'
|
|
89
|
+
'Nothing changes: this repo is never offered and never started for you. Safe default — you keep picking the work.',
|
|
67
90
|
},
|
|
68
91
|
{
|
|
69
92
|
label: 'advisory',
|
|
70
93
|
description:
|
|
71
|
-
'
|
|
94
|
+
'This repo may appear in the ranked suggestions, so you can compare it against the others. You still start each session.',
|
|
72
95
|
},
|
|
73
96
|
{
|
|
74
97
|
label: 'autonomous-gated',
|
|
75
98
|
description:
|
|
76
|
-
'
|
|
99
|
+
'Work may start here without asking, but only when the ranking beats the confidence floor written next to this setting.',
|
|
77
100
|
},
|
|
78
101
|
],
|
|
79
102
|
multiSelect: false,
|
|
@@ -128,7 +151,7 @@ export function isDispatcherAutonomyBlockPresent(claudeMdContent) {
|
|
|
128
151
|
*/
|
|
129
152
|
function coerceAutonomy(value) {
|
|
130
153
|
if (value === null || value === undefined) return 'off';
|
|
131
|
-
const normalized = String(value).toLowerCase().trim();
|
|
154
|
+
const normalized = String(value).toLowerCase().replace(RECOMMENDED_SUFFIX, '').trim();
|
|
132
155
|
return ALLOWED_AUTONOMY.includes(normalized) ? normalized : 'off';
|
|
133
156
|
}
|
|
134
157
|
|
|
@@ -192,9 +192,20 @@ function _parseInlineObject(raw) {
|
|
|
192
192
|
*/
|
|
193
193
|
export function _parseResourceThresholds(kv) {
|
|
194
194
|
return {
|
|
195
|
+
// Memory thresholds are denominated in the signal `evaluate()` actually
|
|
196
|
+
// judges on — memory_pressure > ram_available > ram_free, in that order
|
|
197
|
+
// (#1089). They are NOT compared against Darwin's `os.freemem()` unless
|
|
198
|
+
// nothing better is published, which on Darwin never happens.
|
|
195
199
|
'ram-free-min-gb': _coerceInteger(kv, 'ram-free-min-gb', 4),
|
|
196
200
|
'ram-free-critical-gb': _coerceInteger(kv, 'ram-free-critical-gb', 2),
|
|
197
|
-
|
|
201
|
+
// 80 → 90 (#1089). At 80 this fired on 15.6% of 1477 measured session
|
|
202
|
+
// starts, largely on the decaying tail of the coordinator's own gate run;
|
|
203
|
+
// at 90 it fires on 12.8%, and under the two-signal rule it no longer caps
|
|
204
|
+
// anything on its own. Both numbers are measurements, not preferences.
|
|
205
|
+
'cpu-load-max-pct': _coerceInteger(kv, 'cpu-load-max-pct', 90),
|
|
206
|
+
// Unchanged at 5 — but now compared against LIVE PEER SESSIONS rather than
|
|
207
|
+
// the Claude process count. Same number, different denominator: measured
|
|
208
|
+
// firing rate drops from 93.6% to 4.2% (median processes:sessions = 6.0).
|
|
198
209
|
'concurrent-sessions-warn': _coerceInteger(kv, 'concurrent-sessions-warn', 5),
|
|
199
210
|
'ssh-no-docker': _coerceBoolean(kv, 'ssh-no-docker', true),
|
|
200
211
|
};
|
|
@@ -34,7 +34,7 @@ import path from 'node:path';
|
|
|
34
34
|
|
|
35
35
|
import { scanBacklog } from '../backlog-scan.mjs';
|
|
36
36
|
import { checkCiStatus as realCheckCiStatus } from '../ci-status-banner.mjs';
|
|
37
|
-
import { probe as realProbe, evaluate as realEvaluate } from '../resource-probe.mjs';
|
|
37
|
+
import { probe as realProbe, evaluate as realEvaluate, DEFAULT_RESOURCE_THRESHOLDS as CANONICAL_RESOURCE_THRESHOLDS } from '../resource-probe.mjs';
|
|
38
38
|
import { isRealSession } from '../session-schema/filters.mjs';
|
|
39
39
|
|
|
40
40
|
/** Staleness cap (days). Beyond this, additional age does not raise the score. */
|
|
@@ -49,12 +49,9 @@ const MS_PER_DAY = 86_400_000;
|
|
|
49
49
|
* Used by the default `resourceVerdict` dep so ranking works standalone
|
|
50
50
|
* without a parsed Session Config in hand.
|
|
51
51
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'cpu-load-max-pct': 80,
|
|
56
|
-
'concurrent-sessions-warn': 5,
|
|
57
|
-
};
|
|
52
|
+
// #1089: re-exported from the single canonical definition rather than a third
|
|
53
|
+
// hand-maintained copy — the three copies had already drifted apart.
|
|
54
|
+
const DEFAULT_RESOURCE_THRESHOLDS = { ...CANONICAL_RESOURCE_THRESHOLDS };
|
|
58
55
|
|
|
59
56
|
// ---------------------------------------------------------------------------
|
|
60
57
|
// PURE core
|
|
@@ -36,7 +36,7 @@ const startTime = Date.now();
|
|
|
36
36
|
const tcResult = runCheck(typecheckCmd);
|
|
37
37
|
const tcErrorCount =
|
|
38
38
|
tcResult.status === 'fail'
|
|
39
|
-
? extractCount(tcResult.output, /error TS\d+/)
|
|
39
|
+
? extractCount(tcResult.fullOutput ?? tcResult.output, /error TS\d+/)
|
|
40
40
|
: 0;
|
|
41
41
|
|
|
42
42
|
// --- Test ---
|
|
@@ -46,14 +46,14 @@ const tcErrorCount =
|
|
|
46
46
|
const testResult = runCheck(testCmd);
|
|
47
47
|
const { passed: testPassed, failed: testFailed, total: testTotal } =
|
|
48
48
|
testResult.status !== 'skip'
|
|
49
|
-
? extractTestCounts(testResult.output)
|
|
49
|
+
? extractTestCounts(testResult.fullOutput ?? testResult.output)
|
|
50
50
|
: { passed: 0, failed: 0, total: 0 };
|
|
51
51
|
|
|
52
52
|
// --- Lint ---
|
|
53
53
|
const lintResult = runCheck(lintCmd);
|
|
54
54
|
const lintWarnings =
|
|
55
55
|
lintResult.status !== 'skip'
|
|
56
|
-
? extractCount(lintResult.output, /warning/i)
|
|
56
|
+
? extractCount(lintResult.fullOutput ?? lintResult.output, /warning/i)
|
|
57
57
|
: 0;
|
|
58
58
|
|
|
59
59
|
// --- Debug artifacts ---
|
|
@@ -41,17 +41,28 @@ function isTestFile(filePath) {
|
|
|
41
41
|
/**
|
|
42
42
|
* Execute a shell command and return a structured result.
|
|
43
43
|
*
|
|
44
|
+
* `output` is a bounded TAIL for humans. `fullOutput` is the complete captured
|
|
45
|
+
* text and is what every COUNT parse must read.
|
|
46
|
+
*
|
|
47
|
+
* Why both: the tail was 5 lines and was also the parser's input. Vitest prints
|
|
48
|
+
* its `Test Files` / `Tests` summary and THEN the per-failure detail, so on any
|
|
49
|
+
* real failure the summary sits hundreds of lines above the tail — measured
|
|
50
|
+
* 2026-08-22: line 174 of 936. `extractTestCounts` then found nothing and the
|
|
51
|
+
* gate reported `test: fail, total 0, passed 0, failed 0`, which reads as
|
|
52
|
+
* "the runner never produced results" and hides WHICH test failed. An hour was
|
|
53
|
+
* spent chasing that phantom before the real cause (one red test) was found.
|
|
54
|
+
*
|
|
44
55
|
* @param {string} cmd - Shell command to run, or `"skip"` / empty to skip.
|
|
45
|
-
* @returns {{ status: 'pass'|'fail'|'skip', output: string, exitCode: number, stubbed?: { kind: 'echo'|'noop' } }}
|
|
56
|
+
* @returns {{ status: 'pass'|'fail'|'skip', output: string, fullOutput: string, exitCode: number, stubbed?: { kind: 'echo'|'noop' } }}
|
|
46
57
|
*/
|
|
47
58
|
export function runCheck(cmd) {
|
|
48
59
|
if (!cmd || cmd === 'skip') {
|
|
49
|
-
return { status: 'skip', output: '', exitCode: 0 };
|
|
60
|
+
return { status: 'skip', output: '', fullOutput: '', exitCode: 0 };
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
const stub = detectStubCommand(cmd);
|
|
53
64
|
if (stub.isStub) {
|
|
54
|
-
return { status: 'pass', output: `(stubbed: ${stub.kind})`, exitCode: 0, stubbed: { kind: stub.kind } };
|
|
65
|
+
return { status: 'pass', output: `(stubbed: ${stub.kind})`, fullOutput: `(stubbed: ${stub.kind})`, exitCode: 0, stubbed: { kind: stub.kind } };
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
try {
|
|
@@ -61,18 +72,18 @@ export function runCheck(cmd) {
|
|
|
61
72
|
maxBuffer: RUN_CHECK_MAX_BUFFER_BYTES,
|
|
62
73
|
});
|
|
63
74
|
const output = raw.split('\n').slice(-5).join('\n').trim();
|
|
64
|
-
return { status: 'pass', output, exitCode: 0 };
|
|
75
|
+
return { status: 'pass', output, fullOutput: raw, exitCode: 0 };
|
|
65
76
|
} catch (err) {
|
|
66
77
|
const exitCode = typeof err.status === 'number' ? err.status : 1;
|
|
67
78
|
|
|
68
79
|
// Exit code 127 means command not found — treat as skip.
|
|
69
80
|
if (exitCode === 127) {
|
|
70
|
-
return { status: 'skip', output: 'command not found', exitCode };
|
|
81
|
+
return { status: 'skip', output: 'command not found', fullOutput: '', exitCode };
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
const combined = [err.stdout ?? '', err.stderr ?? ''].join('\n');
|
|
74
85
|
const output = combined.split('\n').slice(-5).join('\n').trim();
|
|
75
|
-
return { status: 'fail', output, exitCode };
|
|
86
|
+
return { status: 'fail', output, fullOutput: combined, exitCode };
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
|