session-orchestrator 4.0.1 → 4.2.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/.agents/skills/session-plan/SKILL.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/session-plan/SKILL.md +1 -1
- package/.cursor/skills/session-plan/SKILL.md +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +57 -0
- package/README.md +55 -51
- package/agents/ux-evaluator.md +1 -1
- package/commands/close.md +3 -3
- package/commands/go.md +2 -0
- package/commands/memory-cleanup.md +4 -3
- package/commands/persona-panel.md +1 -1
- package/commands/release.md +4 -4
- package/commands/session.md +3 -2
- package/docs/README.md +4 -4
- package/docs/USER-GUIDE.md +115 -48
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +55 -1
- package/docs/ci-setup.md +1 -1
- package/docs/codex-setup.md +1 -0
- package/docs/components.md +2 -2
- package/docs/cursor-setup.md +1 -0
- package/docs/events-schema.md +4 -1
- package/docs/instruction-delivery.md +1 -1
- package/docs/memory-proposal-flow.md +3 -3
- package/docs/migration-v4.md +2 -2
- package/docs/owner-config-schema.md +74 -90
- package/docs/persona-panel.md +4 -4
- package/docs/pi-setup.md +1 -0
- package/docs/rule-authoring.md +13 -6
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +55 -22
- package/docs/session-config-template.md +9 -5
- package/docs/vault-docs-architecture.md +4 -2
- package/hooks/_lib/hook-import-set.json +28 -3
- package/hooks/_lib/vcs-create-matcher.mjs +214 -16
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/pre-bash-issue-budget.mjs +123 -26
- package/hooks/subagent-telemetry.mjs +106 -20
- package/package.json +4 -4
- package/scripts/baseline-archetypes.mjs +28 -0
- package/scripts/ci/assert-coverage-green.mjs +100 -0
- package/scripts/lib/auto-dialectic.mjs +0 -68
- package/scripts/lib/baseline-archetypes.mjs +439 -0
- package/scripts/lib/build-live-signals.mjs +5 -6
- package/scripts/lib/config/issue-budget.mjs +68 -8
- package/scripts/lib/config/private-config-dir.mjs +3 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config-schema.mjs +79 -0
- package/scripts/lib/events.mjs +3 -3
- package/scripts/lib/file-lock.mjs +47 -5
- package/scripts/lib/issue-budget-reconcile.mjs +392 -0
- package/scripts/lib/issue-budget.mjs +76 -3
- package/scripts/lib/learnings/evolve-telemetry.mjs +1 -2
- package/scripts/lib/maintenance-due-banner.mjs +440 -0
- package/scripts/lib/owner-config.example.yaml +29 -46
- package/scripts/lib/owner-yaml.mjs +14 -13
- package/scripts/lib/project-hygiene.mjs +182 -6
- package/scripts/lib/quality-gate.mjs +13 -6
- package/scripts/lib/resource-probe/evaluate.mjs +19 -21
- package/scripts/lib/rules-sync.mjs +34 -4
- package/scripts/lib/session-close-backfill.mjs +182 -40
- package/scripts/lib/session-end/phase-skip.mjs +85 -86
- package/scripts/lib/session-end/tail-runner.mjs +178 -0
- package/scripts/lib/session-identity/own-session.mjs +24 -13
- package/scripts/lib/session-schema/constants.mjs +6 -0
- package/scripts/lib/session-schema/validator.mjs +20 -0
- package/scripts/lib/session-shape.mjs +558 -0
- package/scripts/lib/session-start-probes.mjs +10 -3
- package/scripts/lib/session-token-rollup.mjs +95 -10
- package/scripts/lib/state-md/frontmatter-mutators.mjs +22 -34
- package/scripts/lib/state-md.mjs +1 -0
- package/scripts/lib/subagents-schema.mjs +77 -9
- package/scripts/lib/telemetry/pricing.mjs +197 -0
- package/scripts/lib/telemetry/sync.mjs +50 -1
- package/scripts/lib/validate/check-owner-leakage.mjs +17 -8
- package/scripts/lib/validate/check-skill-script-paths.mjs +33 -10
- package/scripts/lib/validate/check-unwired-features.mjs +8 -7
- package/scripts/lib/vault-mirror/process.mjs +2 -1
- package/scripts/lib/vault-mirror/render-sessions.mjs +8 -1
- package/scripts/lib/vault-status/narrative-mirror.mjs +4 -4
- package/scripts/lib/wave-resource-gate.mjs +23 -27
- package/scripts/lib/wave-sizing.mjs +10 -3
- package/scripts/materialize-wave-scope.mjs +68 -14
- package/scripts/print-applicable-rules.mjs +7 -6
- package/scripts/print-learnings-index.mjs +3 -2
- package/scripts/release.mjs +32 -11
- package/scripts/session-shape.mjs +266 -0
- package/skills/_shared/config-reading.md +15 -9
- package/skills/_shared/private-capability-context.md +89 -0
- package/skills/bootstrap/SKILL.md +61 -13
- package/skills/bootstrap/_shared-template.md +99 -14
- package/skills/bootstrap/deep-template.md +36 -26
- package/skills/bootstrap/fast-template.md +44 -8
- package/skills/bootstrap/intensity-heuristic.md +10 -4
- package/skills/bootstrap/private-contract.md +119 -0
- package/skills/bootstrap/public-fallback.md +30 -18
- package/skills/bootstrap/standard-template.md +39 -24
- package/skills/discovery/probes-ui.md +1 -1
- package/skills/docs-orchestrator/audience-mapping.md +1 -1
- package/skills/evolve/SKILL.md +2 -2
- package/skills/gitlab-ops/SKILL.md +3 -3
- package/skills/grill/SKILL.md +1 -1
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/plan/mode-new.md +9 -0
- package/skills/reconcile/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +3 -2
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +23 -65
- package/skills/session-end/phase-3-7a-recommendations.md +2 -2
- package/skills/session-end/references/phase-3-documentation-updates.md +8 -6
- package/skills/session-end/references/phase-5-issue-cleanup.md +26 -0
- package/skills/session-end/session-metrics-write.md +31 -12
- package/skills/session-plan/SKILL.md +56 -48
- package/skills/session-plan/wave-template.md +8 -15
- package/skills/session-start/SKILL.md +18 -2
- package/skills/session-start/phase-2-5-docs-planning.md +1 -1
- package/skills/session-start/phase-8-5-express-path.md +12 -9
- package/skills/session-start/references/phase-1-5-session-continuity.md +2 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +21 -5
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +3 -1
- package/skills/test-runner/rubric-v1.md +2 -2
- package/skills/wave-executor/SKILL.md +42 -12
- package/skills/wave-executor/circuit-breaker.md +3 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +4 -2
- package/skills/wave-executor/references/wave-loop-review.md +1 -1
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +6 -2
- package/templates/nextjs-minimal/package.json +1 -1
- package/templates/node-minimal/package.json +1 -1
- package/scripts/lib/multi-provider-build/providers.mjs +0 -64
- package/scripts/lib/multi-provider-build/templating.mjs +0 -130
- package/scripts/lib/owner-config/coerce.mjs +0 -29
- package/scripts/lib/owner-config/constants.mjs +0 -21
- package/scripts/lib/owner-config/defaults.mjs +0 -50
- package/scripts/lib/owner-config/error.mjs +0 -19
- package/scripts/lib/owner-config/index.mjs +0 -13
- package/scripts/lib/owner-config/merge.mjs +0 -52
- package/scripts/lib/owner-config/validate.mjs +0 -259
- package/scripts/lib/owner-config-loader.mjs +0 -170
- package/scripts/lib/owner-config.mjs +0 -28
- package/scripts/lib/soul-resolve.mjs +0 -130
- package/scripts/lib/vault-mirror/render.mjs +0 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { matchBlockHeader } from './block-header.mjs';
|
|
2
2
|
import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
3
|
+
import { _coerceInteger } from './coercers.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* issue-budget.mjs — Parser for the `issue-budget:` block-style Session Config key.
|
|
@@ -17,10 +18,13 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
17
18
|
* `scripts/lib/spiral-carryover.mjs` `runCli()` (programmatic path), both via
|
|
18
19
|
* `scripts/lib/issue-budget.mjs`.
|
|
19
20
|
*
|
|
20
|
-
* Returns `{ "max-per-session", mode, overflow }`.
|
|
21
|
-
* Tolerant parser: malformed values
|
|
21
|
+
* Returns `{ "max-per-session", "max-per-session-raw", mode, overflow }`.
|
|
22
|
+
* Tolerant parser: malformed values fall back to defaults (the
|
|
22
23
|
* `reconcile.min-rule-days` posture, NOT the noisier handover-gate WARN) —
|
|
23
|
-
* a hook must never spam stderr on every single Bash call.
|
|
24
|
+
* a hook must never spam stderr on every single Bash call. ONE exception, and
|
|
25
|
+
* it is a safety one: a malformed per-session OVERRIDE with a valid base number
|
|
26
|
+
* (`7 (feature: x)`) keeps the base 7 and WARNs on stderr, because falling back
|
|
27
|
+
* to the built-in 12 there would silently LOOSEN a cap the operator tightened.
|
|
24
28
|
*
|
|
25
29
|
* Consumers: `scripts/lib/config.mjs`, `scripts/lib/issue-budget.mjs`,
|
|
26
30
|
* `hooks/pre-bash-issue-budget.mjs`, `skills/session-end/SKILL.md` Phase 5.
|
|
@@ -44,7 +48,9 @@ const OVERFLOW_SINKS = ['collect-issue', 'vault-note'];
|
|
|
44
48
|
* Defaults:
|
|
45
49
|
* max-per-session: 12 integer >= 0; 0 means "no issue may be created"
|
|
46
50
|
* (a valid, deliberately harsh setting). Malformed
|
|
47
|
-
* or negative input falls back to 12
|
|
51
|
+
* or negative input falls back to 12 — EXCEPT a
|
|
52
|
+
* malformed override over a valid base (`7 (x: y)`),
|
|
53
|
+
* which keeps 7 and WARNs.
|
|
48
54
|
* mode: strict strict | warn | off
|
|
49
55
|
* overflow: collect-issue collect-issue | vault-note
|
|
50
56
|
*
|
|
@@ -54,12 +60,29 @@ const OVERFLOW_SINKS = ['collect-issue', 'vault-note'];
|
|
|
54
60
|
* mode: strict
|
|
55
61
|
* overflow: collect-issue
|
|
56
62
|
*
|
|
63
|
+
* NEW POLICY (not a restatement of an existing rule): `max-per-session` accepts
|
|
64
|
+
* the SAME per-session-type override syntax `agents-per-wave` already uses —
|
|
65
|
+
* `12 (feature: 6)` parses via `_coerceInteger` into
|
|
66
|
+
* `{ default: 12, feature: 6 }`. The key set is OPEN (any session-type label
|
|
67
|
+
* the operator writes), because the session-type vocabulary lives in
|
|
68
|
+
* session-start, not here.
|
|
69
|
+
*
|
|
70
|
+
* The parsed override object is returned under `"max-per-session-raw"`, while
|
|
71
|
+
* `"max-per-session"` stays STRICTLY NUMERIC (the `.default`). That split is
|
|
72
|
+
* load-bearing: three consumers read the key as a number
|
|
73
|
+
* (`scripts/lib/issue-budget.mjs` `chargeIssueBudget`,
|
|
74
|
+
* `hooks/pre-bash-issue-budget.mjs`, and the config-parity docs), and handing
|
|
75
|
+
* any of them an object would surface as `[object Object]` in a cap comparison
|
|
76
|
+
* rather than as an error. Resolution against the CURRENT session type is
|
|
77
|
+
* `resolveMaxPerSession()` in `scripts/lib/issue-budget.mjs`.
|
|
78
|
+
*
|
|
57
79
|
* @param {string} content — full file contents
|
|
58
|
-
* @returns {{ "max-per-session": number, mode: string, overflow: string }}
|
|
80
|
+
* @returns {{ "max-per-session": number, "max-per-session-raw": number|{default: number, [k: string]: number}, mode: string, overflow: string }}
|
|
59
81
|
*/
|
|
60
82
|
export function _parseIssueBudget(content) {
|
|
61
83
|
const defaults = {
|
|
62
84
|
'max-per-session': 12,
|
|
85
|
+
'max-per-session-raw': 12,
|
|
63
86
|
mode: 'strict',
|
|
64
87
|
overflow: 'collect-issue',
|
|
65
88
|
};
|
|
@@ -84,6 +107,7 @@ export function _parseIssueBudget(content) {
|
|
|
84
107
|
if (blockLines.length === 0) return { ...defaults };
|
|
85
108
|
|
|
86
109
|
let maxPerSession = defaults['max-per-session'];
|
|
110
|
+
let maxPerSessionRaw = defaults['max-per-session-raw'];
|
|
87
111
|
let mode = defaults.mode;
|
|
88
112
|
let overflow = defaults.overflow;
|
|
89
113
|
|
|
@@ -101,10 +125,45 @@ export function _parseIssueBudget(content) {
|
|
|
101
125
|
else if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) v = v.slice(1, -1);
|
|
102
126
|
|
|
103
127
|
switch (k) {
|
|
104
|
-
case 'max-per-session':
|
|
105
|
-
// Non-negative integer
|
|
106
|
-
|
|
128
|
+
case 'max-per-session': {
|
|
129
|
+
// Non-negative integer, or the `N (type: M)` override form. A leading
|
|
130
|
+
// '-' fails \d+ inside `_coerceInteger`, which throws → default.
|
|
131
|
+
// Tolerant by contract: this parser runs on every Bash call via the
|
|
132
|
+
// hook, so a malformed value must fall back silently, never throw.
|
|
133
|
+
let coerced;
|
|
134
|
+
try {
|
|
135
|
+
coerced = _coerceInteger(new Map([['max-per-session', v]]), 'max-per-session', 12);
|
|
136
|
+
} catch {
|
|
137
|
+
// A malformed OVERRIDE (`7 (feature: x)`) must never restore the
|
|
138
|
+
// built-in 12: the operator wrote a base cap of 7, and discarding the
|
|
139
|
+
// whole value because the parenthesised part is unparseable LOOSENS
|
|
140
|
+
// the cap by three on one typo — silently, in the direction nobody
|
|
141
|
+
// would choose. Keep the base number, drop the override, and say so
|
|
142
|
+
// once (this branch is reachable only on a malformed value, so it
|
|
143
|
+
// cannot become per-Bash-call stderr spam).
|
|
144
|
+
const base = v.match(/^(\d+)\s*\(/);
|
|
145
|
+
if (base) {
|
|
146
|
+
const n = Number.parseInt(base[1], 10);
|
|
147
|
+
if (Number.isInteger(n) && n >= 0) {
|
|
148
|
+
maxPerSession = n;
|
|
149
|
+
maxPerSessionRaw = n;
|
|
150
|
+
process.stderr.write(
|
|
151
|
+
`⚠ issue-budget: malformed per-session override in 'max-per-session': '${v}' — ` +
|
|
152
|
+
`kept the base cap ${n} and dropped the override.\n`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
if (typeof coerced === 'number') {
|
|
159
|
+
maxPerSession = coerced;
|
|
160
|
+
maxPerSessionRaw = coerced;
|
|
161
|
+
} else if (Number.isInteger(coerced?.default) && coerced.default >= 0) {
|
|
162
|
+
maxPerSession = coerced.default;
|
|
163
|
+
maxPerSessionRaw = coerced;
|
|
164
|
+
}
|
|
107
165
|
break;
|
|
166
|
+
}
|
|
108
167
|
|
|
109
168
|
case 'mode':
|
|
110
169
|
if (MODES.includes(v.toLowerCase())) mode = v.toLowerCase();
|
|
@@ -118,6 +177,7 @@ export function _parseIssueBudget(content) {
|
|
|
118
177
|
|
|
119
178
|
return {
|
|
120
179
|
'max-per-session': maxPerSession,
|
|
180
|
+
'max-per-session-raw': maxPerSessionRaw,
|
|
121
181
|
mode,
|
|
122
182
|
overflow,
|
|
123
183
|
};
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* `owner.yaml`, the self-alias ledger) moves with it, so the precedence has to
|
|
6
6
|
* live in exactly ONE place. Before #1223 three copies with three different
|
|
7
7
|
* precedences existed: `owner-yaml.mjs`'s import-time homedir-only
|
|
8
|
-
* `OWNER_YAML_PATH`, `owner-config-loader.mjs`'s XDG-only +
|
|
9
|
-
* `resolveOwnerConfigPath()`, and `host-identity.mjs`'s
|
|
8
|
+
* `OWNER_YAML_PATH`, the now-removed `owner-config-loader.mjs`'s XDG-only +
|
|
9
|
+
* untrimmed `resolveOwnerConfigPath()`, and `host-identity.mjs`'s
|
|
10
|
+
* `_privateDir()`. The
|
|
10
11
|
* live consequence: `SO_CONFIG_HOME=<sandbox>` moved the alias ledger but NOT
|
|
11
12
|
* owner.yaml, which kept being read from the operator's REAL home — the
|
|
12
13
|
* CLAUDE.md (Codex CLI alias: AGENTS.md) "vault-dir resolves HOST-LOCALLY"
|
|
@@ -40,8 +40,8 @@ export const REMOTE_HOST_DEFAULTS = Object.freeze({
|
|
|
40
40
|
* scripts/lib/config.mjs — impl / db / security / compliance / docs stay local.
|
|
41
41
|
*
|
|
42
42
|
* NOT the wave-role enum ("Impl-Core", "Quality", …): those are two different
|
|
43
|
-
* enums and conflating them is the documented trap (see
|
|
44
|
-
* in wave-resource-gate.mjs). The wave→agent-mapping-role translation is
|
|
43
|
+
* enums and conflating them is the documented trap (see the MODE_BLIND_SESSION_TYPE
|
|
44
|
+
* note in wave-resource-gate.mjs). The wave→agent-mapping-role translation is
|
|
45
45
|
* `OFFLOADABLE_WAVE_ROLES` in that same module.
|
|
46
46
|
*
|
|
47
47
|
* @type {readonly string[]}
|
|
@@ -86,6 +86,8 @@ export function validateSessionConfig(config) {
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
validateIssueBudget(config['issue-budget'], errors);
|
|
90
|
+
|
|
89
91
|
validateVaultIntegration(config['vault-integration'], errors);
|
|
90
92
|
validateVaultSync(config['vault-sync'], errors);
|
|
91
93
|
|
|
@@ -124,6 +126,83 @@ export function validateSessionConfig(config) {
|
|
|
124
126
|
return { ok: true, config, warnings };
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Validate the `issue-budget` block's cap, including the per-session-type
|
|
131
|
+
* override form (#session-shape) — `12 (feature: 6)` parses to
|
|
132
|
+
* `{ default: 12, feature: 6 }` and lands on `max-per-session-raw`, while
|
|
133
|
+
* `max-per-session` stays the resolved NUMBER every consumer reads.
|
|
134
|
+
*
|
|
135
|
+
* Mirrors `validateAgentsPerWave` in shape, with two deliberate differences:
|
|
136
|
+
* the floor is 0 (`max-per-session: 0` is a valid "no issue may be created"
|
|
137
|
+
* setting) and the override key set is free-form, because the session-type
|
|
138
|
+
* vocabulary is owned by session-start, not by this schema.
|
|
139
|
+
*
|
|
140
|
+
* @param {unknown} block — `config['issue-budget']`
|
|
141
|
+
* @param {Array<{path: string, rule: string, message: string}>} errors
|
|
142
|
+
*/
|
|
143
|
+
function validateIssueBudget(block, errors) {
|
|
144
|
+
if (block === undefined || block === null) return;
|
|
145
|
+
if (typeof block !== 'object' || Array.isArray(block)) {
|
|
146
|
+
errors.push({
|
|
147
|
+
path: 'issue-budget',
|
|
148
|
+
rule: 'object',
|
|
149
|
+
message: `issue-budget must be an object (got ${JSON.stringify(block)})`,
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const resolved = block['max-per-session'];
|
|
155
|
+
if (resolved !== undefined && (!Number.isInteger(resolved) || resolved < 0)) {
|
|
156
|
+
errors.push({
|
|
157
|
+
path: 'issue-budget.max-per-session',
|
|
158
|
+
rule: 'integer>=0',
|
|
159
|
+
message: `issue-budget.max-per-session must be an integer >= 0 (got ${JSON.stringify(resolved)})`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const raw = block['max-per-session-raw'];
|
|
164
|
+
if (raw === undefined || raw === null) return;
|
|
165
|
+
|
|
166
|
+
if (Number.isInteger(raw)) {
|
|
167
|
+
if (raw < 0) {
|
|
168
|
+
errors.push({
|
|
169
|
+
path: 'issue-budget.max-per-session-raw',
|
|
170
|
+
rule: 'integer>=0',
|
|
171
|
+
message: `issue-budget.max-per-session-raw must be an integer >= 0 (got ${raw})`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
178
|
+
const defaultVal = raw['default'];
|
|
179
|
+
if (!Number.isInteger(defaultVal) || defaultVal < 0) {
|
|
180
|
+
errors.push({
|
|
181
|
+
path: 'issue-budget.max-per-session-raw.default',
|
|
182
|
+
rule: 'integer>=0',
|
|
183
|
+
message: `issue-budget.max-per-session-raw.default must be an integer >= 0 (got ${JSON.stringify(defaultVal)})`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
187
|
+
if (k === 'default') continue;
|
|
188
|
+
if (!Number.isInteger(v) || v < 0) {
|
|
189
|
+
errors.push({
|
|
190
|
+
path: `issue-budget.max-per-session-raw.${k}`,
|
|
191
|
+
rule: 'integer>=0',
|
|
192
|
+
message: `issue-budget.max-per-session-raw.${k} must be an integer >= 0 (got ${JSON.stringify(v)})`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
errors.push({
|
|
200
|
+
path: 'issue-budget.max-per-session-raw',
|
|
201
|
+
rule: 'integer-or-object',
|
|
202
|
+
message: `issue-budget.max-per-session-raw must be an integer >= 0 or an object with numeric entries (got ${JSON.stringify(raw)})`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
127
206
|
function validateAgentsPerWave(value, errors) {
|
|
128
207
|
if (Number.isInteger(value)) {
|
|
129
208
|
if (value < 2) {
|
package/scripts/lib/events.mjs
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
* 3. **Never a peer's id (#1123).** A shared working copy means
|
|
28
28
|
* `session.lock` can name a PEER session that won the acquire race. The
|
|
29
29
|
* lock alone therefore does not prove ownership; the fill happens only
|
|
30
|
-
* when a PROCESS-LOCAL id (`CLAUDE_CODE_SESSION_ID
|
|
31
|
-
* `
|
|
30
|
+
* when a confirmed native PROCESS-LOCAL id (`CLAUDE_CODE_SESSION_ID` or
|
|
31
|
+
* `CODEX_THREAD_ID`) equals the lock's raw `session_id`. STATE.md is NOT a
|
|
32
32
|
* witness here (#1177 FX1): it is a shared working-copy file written by
|
|
33
33
|
* the lock holder, so under a peer-owned lock both agreed about the peer
|
|
34
34
|
* and the union stamped the peer's ids. See {@link attributionForRecord}.
|
|
@@ -124,7 +124,7 @@ const STATE_DIR_CANDIDATES = ['.claude', '.codex', '.cursor', '.pi'];
|
|
|
124
124
|
* when one exists it decides alone.**
|
|
125
125
|
*
|
|
126
126
|
* - No lock (CI, a bare script) → `{}`. Nothing to attribute to.
|
|
127
|
-
* - No process-local id (
|
|
127
|
+
* - No confirmed process-local id (absent or ambiguous native env) → `{}`. Ownership is
|
|
128
128
|
* UNPROVEN, and an unproven attribution is exactly the peer-id write #1123
|
|
129
129
|
* forbids; an absent key costs a correlation, a wrong key costs a false one.
|
|
130
130
|
* - A process-local id that equals the lock's raw `session_id` → fill BOTH
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* and the state-lock / staging-fence / session-lock blocks of session-lock.mjs).
|
|
7
7
|
* Each copy independently re-implemented the same skeleton:
|
|
8
8
|
*
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
12
|
-
* 4.
|
|
13
|
-
* 5.
|
|
9
|
+
* 1. exclusive acquisition guard via `linkSync(tmp, lock + '.acquire')`;
|
|
10
|
+
* 2. atomic create-or-fail via `linkSync(tmp, lock)` (POSIX mutex);
|
|
11
|
+
* 3. on EEXIST → read + parse the existing body;
|
|
12
|
+
* 4. same-host + dead-PID (or unparseable) → guarded override + WARN;
|
|
13
|
+
* 5. release the acquisition guard; live/cross-host holders wait until a deadline;
|
|
14
|
+
* 6. owner-guarded primary-lock release.
|
|
14
15
|
*
|
|
15
16
|
* This module is the single home for that skeleton. It is a near-PURE primitive:
|
|
16
17
|
* it imports ONLY scripts/lib/io.mjs (for writeJsonAtomicSync), the two host
|
|
@@ -27,6 +28,8 @@
|
|
|
27
28
|
* opinion of its own — every divergence between the five copies is a knob.
|
|
28
29
|
* - Cross-host locks are NEVER auto-overridden (PSA-003 hard invariant).
|
|
29
30
|
* - Overrides always go through writeJsonAtomicSync (tmp + renameSync).
|
|
31
|
+
* - #1284 serializes acquisition and takeover: a stale observation must not
|
|
32
|
+
* replace a different process's newly acquired lock.
|
|
30
33
|
*
|
|
31
34
|
* No external dependencies — Node 20+ stdlib + io.mjs only.
|
|
32
35
|
*/
|
|
@@ -258,6 +261,21 @@ function serializeBody(body, indent) {
|
|
|
258
261
|
* overridden via writeJsonAtomicSync and a WARN is emitted. A live holder or a
|
|
259
262
|
* cross-host body returns `{ acquired: false, reason: 'held' }`.
|
|
260
263
|
*
|
|
264
|
+
* Every acquisition pass owns the exclusive sibling `${lockPath}.acquire`
|
|
265
|
+
* from before create/read through any takeover. This prevents a waiter from
|
|
266
|
+
* reading an old holder, observing its exit, then replacing a newer holder.
|
|
267
|
+
* All contenders must use this guarded implementation; legacy writers that
|
|
268
|
+
* ignore the sibling guard cannot participate safely in the same protocol.
|
|
269
|
+
*
|
|
270
|
+
* Crash-liveness tradeoff: the guard is held only for this synchronous pass,
|
|
271
|
+
* not for the caller's critical section. If its owner dies during the pass or
|
|
272
|
+
* cleanup fails, the guard remains and attempts return `held` immediately;
|
|
273
|
+
* withFileLock's normal deadline bounds polling. Even a dead-PID or malformed
|
|
274
|
+
* guard is NEVER stolen, because stale-guard replacement would repeat the same
|
|
275
|
+
* race. Recovery requires quiescing every process that can acquire this lock,
|
|
276
|
+
* verifying the guard is abandoned, then explicitly removing only that sibling.
|
|
277
|
+
* The primary lock retains its existing stale/host/owner protections.
|
|
278
|
+
*
|
|
261
279
|
* The `signalVanished` knob reproduces memory-proposals/store.mjs's distinct
|
|
262
280
|
* third state: when the lock file disappears between the EEXIST and the read
|
|
263
281
|
* (concurrent release race), `{ acquired: false, reason: 'vanished' }` is
|
|
@@ -284,6 +302,30 @@ function serializeBody(body, indent) {
|
|
|
284
302
|
* | { acquired: false, reason: 'held'|'vanished'|'fs-error', existing?: object|null, error?: string }}
|
|
285
303
|
*/
|
|
286
304
|
export function tryAcquireFileLock(lockPath, opts = {}) {
|
|
305
|
+
const guardPath = `${lockPath}.acquire`;
|
|
306
|
+
const guard = createExclusive(guardPath, {
|
|
307
|
+
pid: process.pid,
|
|
308
|
+
host: os.hostname(),
|
|
309
|
+
acquiredAt: new Date().toISOString(),
|
|
310
|
+
kind: 'acquisition-guard',
|
|
311
|
+
}, { indent: 2, tmpPrefix: `${opts.tmpPrefix ?? '.file.lock'}.acquire` });
|
|
312
|
+
if (!guard.ok) {
|
|
313
|
+
return guard.reason === 'exists'
|
|
314
|
+
? { acquired: false, reason: 'held', existing: null }
|
|
315
|
+
: { acquired: false, reason: 'fs-error', error: guard.error };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
return tryAcquireGuardedFileLock(lockPath, opts);
|
|
320
|
+
} finally {
|
|
321
|
+
// Only this pass can own/remove this guard; no acquisition path replaces
|
|
322
|
+
// it. An unlink failure deliberately leaves subsequent attempts blocked.
|
|
323
|
+
try { fs.unlinkSync(guardPath); } catch { /* fail closed; see recovery above */ }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Caller must hold the acquisition guard throughout this synchronous pass. */
|
|
328
|
+
function tryAcquireGuardedFileLock(lockPath, opts) {
|
|
287
329
|
const {
|
|
288
330
|
staleCheck = 'pid',
|
|
289
331
|
staleMs,
|