instar 1.3.481 → 1.3.483

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.
@@ -46,6 +46,7 @@
46
46
 
47
47
  import fs from 'node:fs';
48
48
  import path from 'node:path';
49
+ import { fileURLToPath } from 'node:url';
49
50
  import { checkEli16Overview } from '../../../scripts/eli16-overview-check.mjs';
50
51
 
51
52
  function parseArgs() {
@@ -56,6 +57,11 @@ function parseArgs() {
56
57
  report: null,
57
58
  crossModelReview: null,
58
59
  crossModelReason: null,
60
+ // Decision-Completeness counts (Autonomy Principle 2, Piece 2). Optional —
61
+ // when provided, the spec earns `single-run-completable: true` + the counts.
62
+ frontloadedDecisions: null,
63
+ cheapTags: null,
64
+ contestedCleared: null,
59
65
  };
60
66
  for (let i = 0; i < args.length; i++) {
61
67
  const a = args[i];
@@ -64,6 +70,9 @@ function parseArgs() {
64
70
  else if (a === '--report') out.report = args[++i];
65
71
  else if (a === '--cross-model-review') out.crossModelReview = args[++i];
66
72
  else if (a === '--cross-model-reason') out.crossModelReason = args[++i];
73
+ else if (a === '--frontloaded-decisions') out.frontloadedDecisions = parseInt(args[++i], 10);
74
+ else if (a === '--cheap-tags') out.cheapTags = parseInt(args[++i], 10);
75
+ else if (a === '--contested-cleared') out.contestedCleared = parseInt(args[++i], 10);
67
76
  else {
68
77
  console.error(`Unknown arg: ${a}`);
69
78
  process.exit(1);
@@ -72,22 +81,79 @@ function parseArgs() {
72
81
  if (!out.spec || !out.report || !Number.isFinite(out.iterations)) {
73
82
  console.error(
74
83
  'Usage: write-convergence-tag.mjs --spec PATH --iterations N --report PATH ' +
75
- '[--cross-model-review VALUE] [--cross-model-reason REASON]',
84
+ '[--cross-model-review VALUE] [--cross-model-reason REASON] ' +
85
+ '[--frontloaded-decisions N --cheap-tags N --contested-cleared N]',
76
86
  );
77
87
  process.exit(1);
78
88
  }
79
89
  return out;
80
90
  }
81
91
 
92
+ /**
93
+ * Decision-Completeness convergence criterion 2 (Autonomy Principle 2):
94
+ * a spec CANNOT converge while an unresolved user-decision remains in
95
+ * `## Open questions`. Returns the list of unresolved entry lines (empty = ok).
96
+ *
97
+ * Resolution markers that do NOT count as unresolved:
98
+ * - a none-marker line: `*(none)*`, `(none)`, `None`, `None.`, `N/A`
99
+ * - blockquote commentary (`> …`) explaining the section
100
+ * - blank lines / horizontal rules
101
+ * Anything else with content (e.g. a `- **Q1:** …` bullet or a paragraph posing
102
+ * a question) is an unresolved entry.
103
+ */
104
+ export function findOpenQuestions(specBody) {
105
+ // \b…[^\n]*$ (not \s*$) so heading variants like "## Open questions (round 2)"
106
+ // or "## Open Questions & Decisions" are still recognized — a variant heading
107
+ // must not make the section invisible to the gate (reviewer finding, PR 2).
108
+ const m = specBody.match(/^##\s+Open questions\b[^\n]*$/im);
109
+ if (!m) return []; // no section → nothing parked on the user
110
+ const start = m.index + m[0].length;
111
+ const restAfter = specBody.slice(start);
112
+ const nextHeading = restAfter.search(/^##\s+/m);
113
+ const section = nextHeading === -1 ? restAfter : restAfter.slice(0, nextHeading);
114
+ const NONE_RE = /^\s*[*_]*\(?\s*(none|n\/a)\s*\.?\)?[*_]*\s*$/i;
115
+ return section
116
+ .split('\n')
117
+ .map((l) => l.trim())
118
+ .filter((l) => l.length > 0)
119
+ .filter((l) => !l.startsWith('>')) // blockquote commentary
120
+ .filter((l) => !/^-{3,}$/.test(l)) // horizontal rule
121
+ .filter((l) => !NONE_RE.test(l));
122
+ }
123
+
124
+ // ─── main (guarded so the module is importable for tests) ────────────────
125
+ // fileURLToPath (not URL.pathname) so %-encoded paths (spaces) compare correctly,
126
+ // and realpathSync so a symlinked invocation still matches — a mismatch here
127
+ // would otherwise silently exit 0 having done NOTHING (fail-loud lesson).
128
+ const IS_MAIN = (() => {
129
+ if (!process.argv[1]) return false;
130
+ try {
131
+ return (
132
+ fs.realpathSync(path.resolve(process.argv[1])) ===
133
+ fs.realpathSync(fileURLToPath(import.meta.url))
134
+ );
135
+ } catch {
136
+ // @silent-fallback-ok — an unresolvable argv path simply isn't this module.
137
+ return false;
138
+ }
139
+ })();
140
+ if (IS_MAIN) {
141
+ main();
142
+ }
143
+
144
+ function main() {
82
145
  const {
83
146
  spec: specArg,
84
147
  iterations,
85
148
  report: reportArg,
86
149
  crossModelReview,
87
150
  crossModelReason,
151
+ frontloadedDecisions,
152
+ cheapTags,
153
+ contestedCleared,
88
154
  } = parseArgs();
89
155
 
90
- const ROOT = path.resolve(new URL('../../..', import.meta.url).pathname);
156
+ const ROOT = path.resolve(fileURLToPath(new URL('../../..', import.meta.url)));
91
157
  const specPath = path.resolve(ROOT, specArg);
92
158
  const reportPath = path.resolve(ROOT, reportArg);
93
159
 
@@ -130,6 +196,21 @@ if (!_eli16Result.ok) {
130
196
  process.exit(1);
131
197
  }
132
198
 
199
+ // ─── Open-questions gate (Decision-Completeness, Autonomy Principle 2) ────
200
+ // Convergence criterion 2: a spec CANNOT converge while an unresolved
201
+ // user-decision remains in `## Open questions`. Structural — prose can't skip it.
202
+ const openQuestions = findOpenQuestions(content);
203
+ if (openQuestions.length > 0) {
204
+ console.error(
205
+ `Spec ${specArg} still has ${openQuestions.length} unresolved entr${openQuestions.length === 1 ? 'y' : 'ies'} in ## Open questions:\n` +
206
+ openQuestions.map((q) => ` • ${q.slice(0, 120)}`).join('\n') +
207
+ `\n\nA spec cannot converge while a user-decision is still open (Autonomy Principle 2).\n` +
208
+ `Resolve each into ## Frontloaded Decisions (or a contested-and-surviving\n` +
209
+ `cheap-to-change-after tag), leave the section reading "*(none)*", then re-run.`,
210
+ );
211
+ process.exit(1);
212
+ }
213
+
133
214
  // Parse YAML frontmatter manually (no dependency).
134
215
  // Expect: /^---\n<body>\n---\n<rest>/
135
216
  const FM_RE = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
@@ -143,8 +224,9 @@ if (!match) {
143
224
  }
144
225
  const [, fmBody, rest] = match;
145
226
 
146
- // Strip any existing managed lines (review-* chain + cross-model-review chain)
147
- // so re-runs are idempotent — the field is rewritten, never duplicated.
227
+ // Strip any existing managed lines (review-* chain + cross-model-review chain +
228
+ // the single-run-completable chain) so re-runs are idempotent — the field is
229
+ // rewritten, never duplicated.
148
230
  const preservedLines = fmBody
149
231
  .split('\n')
150
232
  .filter(
@@ -154,7 +236,11 @@ const preservedLines = fmBody
154
236
  !/^\s*review-completed-at\s*:/.test(l) &&
155
237
  !/^\s*review-report\s*:/.test(l) &&
156
238
  !/^\s*cross-model-review\s*:/.test(l) &&
157
- !/^\s*cross-model-review-reason\s*:/.test(l),
239
+ !/^\s*cross-model-review-reason\s*:/.test(l) &&
240
+ !/^\s*single-run-completable\s*:/.test(l) &&
241
+ !/^\s*frontloaded-decisions\s*:/.test(l) &&
242
+ !/^\s*cheap-to-change-tags\s*:/.test(l) &&
243
+ !/^\s*contested-then-cleared\s*:/.test(l),
158
244
  )
159
245
  .join('\n')
160
246
  .trim();
@@ -186,6 +272,25 @@ if (crossModelReview) {
186
272
  }
187
273
  }
188
274
 
275
+ // Decision-Completeness evidence (Autonomy Principle 2). The tag is EARNED:
276
+ // it is only written here, after the open-questions gate above passed, and it
277
+ // carries the reviewer's final-round counts so a downstream reader sees WHAT
278
+ // was frontloaded, not just that a boolean is true.
279
+ if (
280
+ Number.isFinite(frontloadedDecisions) ||
281
+ Number.isFinite(cheapTags) ||
282
+ Number.isFinite(contestedCleared)
283
+ ) {
284
+ newFmLines.push('single-run-completable: true');
285
+ if (Number.isFinite(frontloadedDecisions)) {
286
+ newFmLines.push(`frontloaded-decisions: ${frontloadedDecisions}`);
287
+ }
288
+ if (Number.isFinite(cheapTags)) newFmLines.push(`cheap-to-change-tags: ${cheapTags}`);
289
+ if (Number.isFinite(contestedCleared)) {
290
+ newFmLines.push(`contested-then-cleared: ${contestedCleared}`);
291
+ }
292
+ }
293
+
189
294
  const newFm = newFmLines.join('\n');
190
295
 
191
296
  const newContent = `---\n${newFm}\n---\n${rest}`;
@@ -197,3 +302,4 @@ console.log(
197
302
  ` review-iterations=${iterations}\n` +
198
303
  ` review-report=${reportRel}`,
199
304
  );
305
+ }
@@ -0,0 +1,83 @@
1
+ # Reviewer Prompt — Decision Completeness Perspective
2
+
3
+ You are the decision-completeness reviewer for an instar spec under convergence review.
4
+
5
+ Read these in order:
6
+
7
+ 1. The spec file at {SPEC_PATH}
8
+ 2. Any architectural doc the spec references.
9
+
10
+ Your DECISION-COMPLETENESS perspective (Autonomy Principle 2 — "frontload all user
11
+ decisions into the spec so the agent completes it in a SINGLE autonomous run"): your
12
+ ONLY job is to find every point where the building agent would have to **stop
13
+ mid-run and ask the user**, and force each one to be settled NOW, in the spec —
14
+ because agents build at 100–1000x behind dark/dry-run/read-only phases, a decision is
15
+ cheaper to change *after* a completed run than to stop-and-wait mid-run for.
16
+
17
+ Enumerate every mid-run stop-and-ask point. Each MUST be one of:
18
+
19
+ 1. **Frontloaded** — pulled into the spec's `## Frontloaded Decisions` section as a
20
+ concrete decision (made by the author, attributable, reversible-or-not stated). A
21
+ decision the user must make belongs in `## Open questions` — and the spec CANNOT
22
+ converge while any remain (the convergence criterion enforces this; your job is to
23
+ FIND the buried ones that never made it into either section).
24
+
25
+ 2. **Cheap-to-change-after** — explicitly tagged as a default safe to pick now and
26
+ change post-run, *because* the work ships behind a named dark / dry-run /
27
+ read-only phase.
28
+
29
+ **CONTEST every cheap-to-change-after tag.** Do not merely check that a phase is
30
+ named — independently assert reversibility. A closed NON-CHEAP taxonomy overrides any
31
+ tag: anything touching
32
+
33
+ - **durable external side-effects** (sent messages, published pages, external-system
34
+ writes, deleted data),
35
+ - **money** (spend, billing, subscriptions),
36
+ - **identity** (keys, principals, operator binding, trust levels), or
37
+ - **a published / user-visible interface** (API contracts others consume, dashboards
38
+ users rely on, on-disk formats other tools read)
39
+
40
+ is NEVER cheap-to-change-after, regardless of a "ships dark" label — the dark phase
41
+ ends, and what happened during it does not un-happen. A contested tag you reject is a
42
+ **MATERIAL finding that blocks convergence** — same authority as any other material
43
+ issue.
44
+
45
+ Specifically check:
46
+
47
+ 1. **Buried decisions** — sentences like "the implementer can decide", "TBD", "we
48
+ could either X or Y", "depending on user preference", "ask the user when" anywhere
49
+ in the body. Each is an un-frontloaded decision hiding outside `## Open questions`.
50
+
51
+ 2. **Unstated defaults** — config values, thresholds, naming, storage choices the
52
+ spec leaves open. Each needs a concrete default (frontloaded) or a contested-and-
53
+ surviving cheap tag.
54
+
55
+ 3. **Conditional scope** — "if X turns out to be true, then…" branches whose
56
+ resolution requires a human answer mid-build. Force the branch to be decided now
57
+ or restructured so either outcome is buildable without asking.
58
+
59
+ 4. **Approval points** — any step where the spec says to pause for sign-off mid-run
60
+ (deploy gates, "confirm with the user before…"). Distinguish a genuine
61
+ operator-only authority (legitimate — but then the spec must scope the single run
62
+ to END before it, with the gate as the run boundary) from ceremony (a material
63
+ finding: remove it or convert it to a post-run review).
64
+
65
+ 5. **Cheap-tag audit** — for every existing cheap-to-change-after tag: does the named
66
+ dark/dry-run/read-only phase actually exist in the spec's rollout plan? Does the
67
+ tagged decision hit the non-cheap taxonomy above? Reject violators.
68
+
69
+ 6. **Open-questions hygiene** — does `## Open questions` exist, and is every entry a
70
+ genuine user decision (not a design question the author should answer)? An
71
+ author-answerable question parked on the user is a deferral, not a decision.
72
+
73
+ Produce a SHORT report (under 400 words):
74
+
75
+ - **Verdict: CLEAN, MINOR ISSUES, or SERIOUS ISSUES**
76
+ - Counts: `frontloaded-decisions: N`, `cheap-tags: N`, `contested-then-cleared: N`,
77
+ `contested-rejected: N`, `open-user-decisions: N`
78
+ - Specific findings with spec-section references and concrete resolutions (which
79
+ section the decision must move to, what default you recommend, which cheap tag you
80
+ reject and why).
81
+
82
+ Be rigorous. A spec converges only when an agent could complete it in ONE autonomous
83
+ run without a single mid-run "what do you want me to do?"
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-10T22:30:23.798Z",
5
- "instarVersion": "1.3.481",
4
+ "generatedAt": "2026-06-10T23:23:57.878Z",
5
+ "instarVersion": "1.3.483",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -1522,7 +1522,7 @@
1522
1522
  "type": "subsystem",
1523
1523
  "domain": "server",
1524
1524
  "sourcePath": "src/server/AgentServer.ts",
1525
- "contentHash": "7eed149592b598b2ffbea3da927113b4888579d2b53aadda2dcdf091e0457f59",
1525
+ "contentHash": "781f517503b3cf19fa679f1acd223e87cbbeeab44fb16336ab22f2fa5137e005",
1526
1526
  "since": "2025-01-01"
1527
1527
  },
1528
1528
  "subsystem:session-manager": {
@@ -0,0 +1,26 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Conforms the Blocker Ledger (PR #1055) to the developmentAgent dark-feature gate standard (PR #1056) — the two merged from divergent branch points within minutes, so the new `lint-dev-agent-dark-gate` never saw #1055's hardcoded `monitoring.blockerLedger.enabled: false` default, leaving main latently lint-red. Fix: ConfigDefaults omits `enabled`, `AgentServer` resolves it via `resolveDevAgentGate` (LIVE on a development agent for dogfooding, DARK on the fleet → `/blockers` routes 503), the config type makes `enabled` optional, and the feature is registered in `DEV_GATED_FEATURES` with its safety justification (signal-only, no egress, no destructive action; one bounded fail-closed B17 settle check). Fleet behavior is unchanged; existing agents that already received the explicit `enabled: false` default keep it (applyDefaults adds only missing keys — no surprise activation).
9
+
10
+ ## What to Tell Your User
11
+
12
+ - **Development agents only:** "My Blocker Ledger is now live on me (the dev agent) for dogfooding — when I hit something that feels like a wall, it's logged and worked through a pipeline instead of becoming an excuse. Fleet agents are unaffected; it stays off for them until it matures."
13
+ - **Fleet agents:** None — no behavior change (the ledger stays dark until explicitly enabled).
14
+
15
+ ## Summary of New Capabilities
16
+
17
+ | Capability | How to Use |
18
+ |-----------|-----------|
19
+ | Blocker Ledger live-on-dev (dogfood) | Automatic on agents with `developmentAgent: true` — no config edit needed; explicit `monitoring.blockerLedger.enabled: true/false` still wins |
20
+
21
+ ## Evidence
22
+
23
+ - `node scripts/lint-dev-agent-dark-gate.js`: clean (was failing with `[C: unclassified dark default]` at `src/config/ConfigDefaults.ts:257`).
24
+ - `tests/unit/devGatedFeatures-wiring.test.ts`: green with the new `blockerLedger` registry entry — proves live-on-dev / dark-on-fleet resolution against real ConfigDefaults.
25
+ - Blocker Ledger unit (35) + integration (7) + e2e (4): green, unchanged.
26
+ - `npx tsc --noEmit`: exit 0.
@@ -0,0 +1,28 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Implements Piece 2 of `docs/specs/AUTONOMY-PRINCIPLES-ENFORCEMENT-SPEC.md` (Autonomy Principle 2 — frontload all user decisions so a spec completes in ONE autonomous run). Three parts, all in the agent-private spec-converge skill (no runtime `src/` surface):
9
+
10
+ 1. **New internal reviewer** (`reviewer-decision-completeness.md`, 6th of six): enumerates every mid-run stop-and-ask-the-user point; each must be frontloaded into `## Frontloaded Decisions` or tagged cheap-to-change-after behind a named dark/dry-run/read-only phase. The reviewer CONTESTS every cheap tag against a closed non-cheap taxonomy (durable external side-effects, money, identity, published/user-visible interface — never cheap); a rejected tag is a material finding that blocks convergence. Applies to ALL specs (D7), no per-spec override (D11).
11
+ 2. **New convergence criterion** (Phase 3): a spec cannot converge while `## Open questions` contains an unresolved user-decision.
12
+ 3. **Structural enforcement + earned evidence** (`write-convergence-tag.mjs`): the tag writer refuses to stamp `review-convergence` while open questions remain, and writes `single-run-completable: true` plus the reviewer's counts (`frontloaded-decisions`, `cheap-to-change-tags`, `contested-then-cleared`) — the tag carries its evidence, earned not minted. Pre-existing converged specs are unaffected (the gate fires at stamp time only).
13
+
14
+ The spec's migration decision is resolved and recorded: spec-converge stays **agent-private** (matching `/instar-dev`'s not-user-facing status) — deliberately NOT promoted to the fleet builtin skill set, so no fleet migration surface exists.
15
+
16
+ ## What to Tell Your User
17
+
18
+ None — internal change (no user-facing surface).
19
+
20
+ ## Summary of New Capabilities
21
+
22
+ None — internal change (no user-facing surface).
23
+
24
+ ## Evidence
25
+
26
+ - `tests/unit/write-convergence-tag-decision-completeness.test.ts`: 15 new tests — none-marker variants, section scoping, refuse-on-live-question (tag NOT written), stamp-on-resolved, earned counts, no-counts → no minted tag, idempotent re-runs.
27
+ - `tests/unit/write-convergence-tag-crossmodel.test.ts`: 16 existing tests green (no regression from the import-safe main-guard restructure).
28
+ - Live functional run: refused a spec with `- **Q1:** should we do A or B?` (exit 1 + remediation message); stamped `single-run-completable: true` + counts once the section read `*(none)*`.
@@ -0,0 +1,61 @@
1
+ # Side-Effects Review — Blocker Ledger dev-gate registration (PR-1055 follow-up fix)
2
+
3
+ **Version / slug:** `blocker-ledger-dev-gate`
4
+ **Date:** `2026-06-10`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (4-file conformance fix to an enforced standard; no new decision logic)`
7
+
8
+ ## Summary of the change
9
+
10
+ Registers the Blocker Ledger (merged in PR #1055) under the **developmentAgent dark-feature gate standard** enforced by `lint-dev-agent-dark-gate` (PR #1056). The two PRs merged within minutes of each other from divergent branch points, so PR #1055's hardcoded `monitoring.blockerLedger.enabled: false` default never met the new lint in CI — leaving main latently red (the next branch off main fails `npm run lint`). This fix conforms the feature to the standard: ConfigDefaults **omits** `enabled` (`blockerLedger: {}`), `AgentServer` resolves it via `resolveDevAgentGate(...)` (LIVE on a development agent for dogfooding, DARK on the fleet → routes 503), the config type makes `enabled` optional, and the feature is registered in `DEV_GATED_FEATURES` with the required safety justification (signal-only, no egress, no destructive action; one bounded fail-closed ≤200-token B17 check on the rare settle).
11
+
12
+ Files: `src/config/ConfigDefaults.ts`, `src/server/AgentServer.ts`, `src/core/types.ts`, `src/core/devGatedFeatures.ts`.
13
+
14
+ ## Decision-point inventory
15
+
16
+ - `AgentServer` BlockerLedger construction condition — **modify** — from a literal `enabled === true` check to `resolveDevAgentGate(enabled, config)`. The gate's semantics: explicit `true`/`false` wins; absent → live on dev agents, dark on fleet. No other decision point touched.
17
+
18
+ ---
19
+
20
+ ## 1. Over-block
21
+
22
+ No block/allow surface — over-block not applicable. (Fleet behavior is unchanged: absent flag still resolves false on non-dev agents → routes 503 exactly as before.)
23
+
24
+ ## 2. Under-block
25
+
26
+ No block/allow surface — under-block not applicable. The one behavior change is intended: development agents now run the ledger live without a config edit (the dogfood-on-dev standard).
27
+
28
+ ## 3. Level-of-abstraction fit
29
+
30
+ Correct layer: this moves the feature onto the EXISTING gate primitive (`resolveDevAgentGate`) + registry (`DEV_GATED_FEATURES`) rather than a parallel mechanism. The wiring test (`devGatedFeatures-wiring.test.ts`) auto-covers the new entry with real ConfigDefaults.
31
+
32
+ ## 4. Signal vs authority compliance
33
+
34
+ - [x] No — this change has no block/allow surface.
35
+
36
+ The gate resolution is configuration mechanics, not a judgment decision. The ledger's own signal-only posture (PR #1055 artifact) is unchanged.
37
+
38
+ ## 5. Interactions
39
+
40
+ - **Shadowing/double-fire/races:** none — same single construction site, same null-→503 path.
41
+ - **Migration interplay:** `applyDefaults` adds only MISSING keys; an existing agent that already received `blockerLedger: { enabled: false }` from PR #1055's default keeps that explicit false (fleet-safe; dev agents that want live can clear it or set true). New installs get the empty object → gate decides. No surprise activation on the fleet.
42
+
43
+ ## 6. External surfaces
44
+
45
+ None beyond the intended one: a development agent's `/blockers` routes go live (local Bearer-auth API + dashboard tab). Zero egress; no fleet-visible change.
46
+
47
+ ## 7. Rollback cost
48
+
49
+ Trivial: revert restores the literal `enabled: false` default. No persistent state created by the gate itself.
50
+
51
+ ## Conclusion
52
+
53
+ A 4-file conformance fix that repairs the PR #1055 × #1056 merge race (latent red main) and lands the feature on the correct dev-gate standard — which also supersedes the manual dogfood config flip on the dev agent. Verified: `lint-dev-agent-dark-gate` clean, `tsc` clean, dev-gate wiring test green (proves live-on-dev/dark-on-fleet with real defaults), Blocker Ledger unit (35) + integration (7) + e2e (4) all green. Clear to ship.
54
+
55
+ ---
56
+
57
+ ## Evidence pointers
58
+
59
+ - `node scripts/lint-dev-agent-dark-gate.js` → clean (was: `[C: unclassified dark default]` on ConfigDefaults.ts:257).
60
+ - `tests/unit/devGatedFeatures-wiring.test.ts` → green including the new `blockerLedger` entry.
61
+ - `tests/unit/BlockerLedger.test.ts` (28) + `tests/unit/blockerSettleAuthority.test.ts` (7) + `tests/integration/blocker-ledger-routes.test.ts` (7) + `tests/e2e/blocker-ledger-lifecycle.test.ts` (4) → green.
@@ -0,0 +1,85 @@
1
+ # Side-Effects Review — Decision-Completeness Gate in spec-converge (Autonomy Principles Enforcement, Piece 2)
2
+
3
+ **Version / slug:** `decision-completeness-gate`
4
+ **Date:** `2026-06-10`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `independent reviewer subagent — concern raised, all items resolved (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ Implements Piece 2 of `docs/specs/AUTONOMY-PRINCIPLES-ENFORCEMENT-SPEC.md`: makes single-run-completability **provable** in spec-converge. Three parts:
11
+
12
+ 1. **New internal reviewer** (`templates/reviewer-decision-completeness.md`, 6th of six): enumerates every mid-run stop-and-ask-the-user point; each must be **frontloaded** into `## Frontloaded Decisions` or tagged **cheap-to-change-after** behind a named dark/dry-run/read-only phase. The reviewer **CONTESTS every cheap tag** against a closed non-cheap taxonomy (durable external side-effects, money, identity, published/user-visible interface — NEVER cheap); a rejected tag is a material finding that blocks convergence.
13
+ 2. **New convergence criterion** (SKILL.md Phase 3): a spec cannot converge while `## Open questions` contains an unresolved user-decision — additive to "no material new issues."
14
+ 3. **Structural enforcement + earned evidence** (`write-convergence-tag.mjs`): the tag writer now REFUSES to stamp `review-convergence` while open questions remain (criterion 2 cannot be skipped by prose), and writes `single-run-completable: true` + the reviewer's counts (`frontloaded-decisions`, `cheap-to-change-tags`, `contested-then-cleared`) so the tag carries its evidence — earned, not minted. The script gained an import-safe main guard so the parser (`findOpenQuestions`) is unit-testable.
15
+
16
+ Files: `skills/spec-converge/SKILL.md`, `skills/spec-converge/templates/reviewer-decision-completeness.md` (new), `skills/spec-converge/scripts/write-convergence-tag.mjs`, `tests/unit/write-convergence-tag-decision-completeness.test.ts` (new, 15 tests).
17
+
18
+ ## Decision-point inventory
19
+
20
+ - `write-convergence-tag.mjs` open-questions gate — **add** — a deterministic, commit-time text validator: refuses the convergence stamp while `## Open questions` has unresolved entries. (Hard-invariant class, see §4.)
21
+ - Convergence criterion 2 (SKILL.md) — **add** — prose criterion backed by the structural gate above.
22
+ - Decision-Completeness reviewer — **add** — produces findings (signals) folded into the round; blocking authority remains the convergence process itself.
23
+ - `single-run-completable` frontmatter — **add** — disclosure only; does NOT change /instar-dev's `review-convergence` + `approved` enforcement.
24
+
25
+ ---
26
+
27
+ ## 1. Over-block
28
+
29
+ The open-questions gate could refuse a spec whose `## Open questions` section contains commentary that *looks* unresolved. Mitigated: blockquote lines (`>`), none-markers (`*(none)*`, `(none)`, `None`, `None.`, `N/A`, emphasis variants), blank lines, and horizontal rules are all recognized as resolved; only contentful entries refuse. Unit-tested per variant. Residual: a spec author writing prose commentary as plain (non-blockquote) text under Open questions gets refused — acceptable, the error message names the fix and the convention (blockquote commentary) is already what existing specs use (this very spec's Piece-1 sibling used `> Per Principle 2…` + `*(none)*`, which passes).
30
+
31
+ ## 2. Under-block
32
+
33
+ Two honest gaps, both by design: (a) the gate validates the SECTION, not the whole document — a user-decision buried in prose outside `## Open questions` is the REVIEWER's job to find (LLM judgment), not the deterministic gate's; (b) an author could delete the question instead of resolving it — but the Decision-Completeness reviewer re-reads the full spec every round and the deleted-but-unresolved decision resurfaces as a finding (the same "rewriting the spec to hide findings" anti-pattern the skill already documents and catches).
34
+
35
+ ## 3. Level-of-abstraction fit
36
+
37
+ Correct: the deterministic part (is the section empty?) is a cheap structural validator at the tag-writer boundary — the same layer as the existing ELI16-presence check it sits beside. The judgment part (is this REALLY cheap-to-change-after? is that REALLY the user's decision?) lives in the LLM reviewer. Neither re-implements the other.
38
+
39
+ ## 4. Signal vs authority compliance
40
+
41
+ **Required reference:** docs/signal-vs-authority.md
42
+
43
+ - [x] No — the new blocking surface is **hard-invariant validation at a tool boundary**, not a judgment gate.
44
+
45
+ The open-questions gate is the "structural validators at the boundary of the system" case the principle explicitly allows (like "this field must be a number"): it checks section emptiness against an enumerable marker set — zero judgment, deterministic, with a clear remediation message. The *judgment* calls (contesting cheap tags, finding buried decisions) are the reviewer's — and the reviewer produces findings (signals) that block only through the existing convergence process, exactly like the other five reviewers. No brittle check gained judgment authority.
46
+
47
+ ## 5. Interactions
48
+
49
+ - **Shadowing:** the gate runs after the ELI16 check inside the same script — ordering is irrelevant (both must pass; neither consumes the other's input).
50
+ - **Existing callers:** `write-convergence-tag.mjs` is invoked only by the spec-converge skill flow. Its existing tests (`write-convergence-tag-crossmodel.test.ts`, 5 tests) pass unchanged — the new args are optional and the main-guard restructure preserves CLI behavior (IS_MAIN hardened with realpath + fileURLToPath after second-pass review so a symlinked or %-encoded invocation cannot silently no-op).
51
+ - **Pre-existing converged specs:** unaffected — the gate fires at stamp time only; already-stamped specs are never re-validated. Specs converged before this ships simply lack `single-run-completable` (honest, documented in SKILL.md).
52
+ - **Idempotency:** re-runs strip and rewrite the new fields exactly like the review-* chain (tested).
53
+
54
+ ## 6. External surfaces
55
+
56
+ None at runtime. This is skill-content + a repo script + tests: no `src/` change, no API, no fleet migration surface (spec-converge is **agent-private** — deliberately NOT in the builtin skill set, matching `/instar-dev`'s explicit not-user-facing status; the spec's "promote vs declare agent-private" decision is resolved as **agent-private**, recorded here). Future instar-dev specs converge under the stricter criteria — that is the intended effect.
57
+
58
+ ## 7. Rollback cost
59
+
60
+ Trivial: revert the commit. No persistent state, no migration, no user-visible runtime surface. Specs stamped in the interim keep their earned fields (harmless disclosure).
61
+
62
+ ## Conclusion
63
+
64
+ Piece 2 lands the spec's design with the criterion enforced structurally (Structure > Willpower) rather than as prose: the tag writer is now the chokepoint that makes "a spec cannot converge with open user-decisions" unskippable, and the `single-run-completable` tag carries its evidence counts. The one new blocking surface is hard-invariant validation, explicitly inside the principle's allowed class. Verified by 15 new unit tests + the 16 existing cross-model tag tests + live functional runs (refuses on a live question; stamps with counts once resolved). Clear to ship as PR 3-of-3's sibling (PR 2).
65
+
66
+ ---
67
+
68
+ ## Second-pass review
69
+
70
+ **Reviewer:** independent reviewer subagent (adversarial audit of artifact + code + tests, ran the suites).
71
+ **Independent read of the artifact: concern raised → all resolved this pass.**
72
+
73
+ - MUST-FIX (resolved): the IS_MAIN guard could silently exit 0 (doing nothing) when invoked via a symlink or a %-encodable path — safe direction (tag never written without the checks) but a fail-loud violation. Fixed: realpathSync + fileURLToPath comparison; the pre-existing `ROOT` URL-pathname decode bug rode the same fix.
74
+ - MUST-FIX (resolved): this artifact overstated the existing cross-model test count (claimed 16; vitest counts 5). Corrected above.
75
+ - NICE-TO-HAVE (applied): heading-variant false-pass (`## Open questions (round 2)` was invisible to the gate) — regex loosened to `\b[^\n]*$`.
76
+ - NICE-TO-HAVE (applied): SKILL.md now scopes the "earned" claim honestly (the structural guarantee is the open-questions invariant; the counts are caller-supplied disclosure on the same trust model as `--cross-model-review`) and names the blockquote-commentary convention's limit with its reviewer backstop.
77
+ - Confirmed clean: signal-vs-authority compliance (deterministic hard-invariant validation; all judgment in the LLM reviewer), taxonomy fidelity to the spec, and test honesty (refuse → exit 1 AND tag absent; idempotency).
78
+
79
+ ---
80
+
81
+ ## Evidence pointers
82
+
83
+ - `tests/unit/write-convergence-tag-decision-completeness.test.ts` — 15 tests: parser none-marker variants, section-scoping, refuse-on-live-question (and tag NOT written), stamp-on-resolved, earned counts, no-counts → no minted tag, idempotent re-runs.
84
+ - `tests/unit/write-convergence-tag-crossmodel.test.ts` — 16 existing tests green (no regression from the main-guard restructure).
85
+ - Live functional run: refused `- **Q1:** should we do A or B?` with exit 1 + remediation message; stamped `single-run-completable: true` + counts on the `*(none)*` variant.