instar 0.28.96 → 0.28.98

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.
@@ -1,61 +1,68 @@
1
- # Side-Effects Review — Compaction-resume payload carries real context
2
-
3
- **Version / slug:** `NEXT` (will be assigned on publish)
4
- **Date:** `2026-04-17`
5
- **Author:** `echo`
6
- **Second-pass reviewer:** `(this is a prompt-shape fix with no Guard surface — second pass not required per skill Phase 5)`
7
-
8
- ## Summary of the change
9
-
10
- Replaces the one-sentence `COMPACTION_RESUME_PROMPT` constant with a payload built from `topicMemory.formatContextForSession(topicId, 20)` (summary + last 20 real messages + search hint). For Slack and TopicMemory-not-ready fallback, the same payload is built from inline log entries via a new `formatInlineHistory` helper. A `prepareInjectionText` helper writes payloads over 500 chars to `/tmp/instar-compaction-resume/` and replaces the inject with a "read this file immediately" reference — matching the `spawnSessionForTopic` bootstrap pattern.
11
-
12
- Slack recovery path picks up `findLastRealMessage` as well, so proxy standbys / delivery acks newer than the user's message no longer mask unanswered state.
13
-
14
- ## Over-block risk
15
-
16
- **None.** The decision of whether to recover is unchanged — `lastReal?.fromUser` still gates re-injection on both paths. The change is purely in the *content* of the injected text. No new filter, no new gate, no new "do not recover" conditions.
17
-
18
- ## Under-block risk
19
-
20
- **None.** The module doesn't classify or authorize anything; it builds strings. The existing InputGuard check in `sessionManager.injectMessage` still applies — the Telegram inject still starts with `[telegram:${topicId}]` so provenance is `verified`, bypassing the Layer 1.5 injection-pattern check (unchanged behavior).
21
-
22
- ## Level-of-abstraction fit
23
-
24
- The new module sits at `src/messaging/shared/` alongside `isSystemOrProxyMessage.ts` — same layer as other prompt/classifier helpers shared between the Telegram and Slack messaging paths. It does **not** reach into `SessionManager` internals, does **not** reinvent `InputGuard` logic, and does **not** read/write any state-dir files. It only writes to `/tmp`, which matches the `spawnSessionForTopic` bootstrap pattern (`/tmp/instar-telegram/`).
25
-
26
- ## Signal-vs-authority compliance
27
-
28
- Not applicable. The preamble is a **prompt to the agent**, not a gate with blocking authority. It can only influence; the agent still decides what to say. There is no downstream check that treats the preamble's output as authoritative.
29
-
30
- ## Interactions
31
-
32
- - **`isSystemOrProxyMessage` / `findLastRealMessage`** — now used on both Telegram and Slack recovery paths. Existing contract preserved: filters system/proxy entries, walks backward, returns last real entry. Slack log entries have the same `{ text, fromUser, timestamp }` shape the classifier expects.
33
- - **`topicMemory.formatContextForSession`** — already used on the session-spawn path. The compaction-resume path now calls the same function with the same args (`recentLimit=20`). No new dependency; no new failure modes.
34
- - **`sessionManager.injectMessage`** — receives a larger text in the common case. Input handling unchanged: bracketed-paste mode for multi-line content. The 500-char file-threshold keeps us inside the spawn-path's tested envelope.
35
- - **`PresenceProxy`** — unaffected. Continues to call `isSystemOrProxyMessage` unchanged.
36
- - **`CompactionSentinel`** — unaffected. It calls `recoverCompactedSession` with the same signature and interprets the boolean return value the same way.
37
- - **`checkLogForAgentResponse`** — unaffected. Same classifier delegate, same log-scan semantics.
38
- - **`InputGuard` Layer 2 (coherence review)** — the new payload has more context, which may change LLM verdicts at the margin, but the review is non-blocking (inject already happened) and only emits a warning message. No blocking impact.
39
-
40
- ## Rollback cost
41
-
42
- **Low.** The new module is purely additive — nothing else depends on it. To roll back: revert `src/commands/server.ts` to use the one-line `COMPACTION_RESUME_PROMPT` constant, delete `src/messaging/shared/compactionResumePayload.ts`, delete `tests/unit/compactionResumePayload.test.ts`. No schema changes, no state-file migrations, no API contract changes.
43
-
44
- ## File-system footprint
45
-
46
- The file-reference path creates one file per compaction event in `/tmp/instar-compaction-resume/resume-<topicId>-<ts>-<uuid>.txt`. `/tmp` is cleaned on macOS reboot; files are small (one summary + ~20 messages, typically <10KB). For bounded hygiene we rely on the OS reboot sweep — same as the existing `/tmp/instar-telegram/` and `/tmp/instar-paste/` dirs. No new disk-leak risk.
47
-
48
- ## Test coverage
49
-
50
- `tests/unit/compactionResumePayload.test.ts` (12 tests) covers:
51
- - Preamble shape and empty-context handling
52
- - Inline history formatting (sender, timestamp, truncation, missing fields)
53
- - File-threshold cutover + stub language
54
- - Topic 6795 regression shape — user's verbatim last message survives the pipeline
55
-
56
- Existing `tests/unit/isSystemOrProxyMessage.test.ts` (25 tests) still green — the walk-back contract now used on both recovery paths is unchanged.
57
-
58
- ## Failure modes explicitly left unfixed
59
-
60
- - **Huge conversation summaries.** If `topicMemory`'s rolling summary itself exceeds some absurd size, the file-reference payload could grow large. We cap at `recentLimit=20` messages + summary, so in practice this tops out at ~20–50 KB. Not a concern at current scale; will revisit if we hit it.
61
- - **Racing compaction events.** If two triggers (PreCompact hook + watchdog poll) fire within milliseconds, `CompactionSentinel` already dedupes — unchanged here.
1
+ # Side-effects review — validator: tighten FIX_PATTERNS to reduce false positives
2
+
3
+ **Scope**: Three `FIX_PATTERNS` in `upgrade-guide-validator.mjs` were matching
4
+ technical prose that was not claiming a bug fix, causing the publish CI to
5
+ block with a false "no Evidence section" error on the Phase 1b connect-the-dots
6
+ release (NEXT.md v0.28.96).
7
+
8
+ **Root cause**: The NEXT.md for the Phase 1b connect-the-dots release used
9
+ three words in technical contexts that the validator treated as bug-fix claims:
10
+ - `broken` — "while a child is broken" (describes a runtime state, not a defect
11
+ we fixed)
12
+ - `crashed` — "a crashed runner" (adjective, describes a process that exited
13
+ non-zero, not a crash we fixed)
14
+ - `resolve` — "items resolve" and `resolve-conflict` (validation-check language
15
+ and API action-verb name, not "I fixed an issue")
16
+
17
+ **Files touched**:
18
+ - `scripts/upgrade-guide-validator.mjs` — three pattern changes:
19
+ 1. `/\bcrashed\b/i` removed. `crashes?` and `crashing` (verb forms) are kept.
20
+ 2. `/\bbroken\b/i` tightened to `/\b(?:was|were)\s+broken\b/i` — only the
21
+ predicate form ("X was broken") triggers; attributive uses ("a broken
22
+ pipeline") and present-state descriptions ("is broken") do not.
23
+ 3. `/\bresolves?\b/i` tightened to `/\bresolves?\s+(?:an?\b|the\b)/i` — only
24
+ "resolves a/an/the <defect>" triggers; bare "resolve" as a technical verb
25
+ ("items resolve") and hyphenated action verbs ("resolve-conflict") do not.
26
+ - `tests/unit/upgrade-guide-evidence.test.ts` — three regression tests added,
27
+ one per false-positive pattern.
28
+
29
+ **Under-block**: Three narrow edge cases are no longer caught:
30
+
31
+ 1. `crashed` standalone past tense: "the server crashed when X" without also
32
+ containing "fix" or another trigger. In practice any guide describing a crash
33
+ fix would also say "fixed a crash" or "fixes the crash" — caught by the
34
+ existing `\bfix(es|ed|ing)?\b` pattern. Removal of `crashed` alone is
35
+ low-risk.
36
+
37
+ 2. `broken` as attributive adjective or present state: "fixed the broken poller"
38
+ still triggers because "fixed" catches it. "X is broken (and we fixed it)"
39
+ would also need "fixed" or another trigger — acceptable.
40
+
41
+ 3. `resolves` without article: "Resolves race condition" (unusual English, almost
42
+ always "Resolves the/a race condition"). The tightened pattern retains all
43
+ natural phrasing.
44
+
45
+ **Over-block**: None introduced. The change makes the validator more permissive.
46
+ Historical guide warnings decrease slightly.
47
+
48
+ **Level-of-abstraction fit**: `claimsFix` is a pure heuristic signal. The gate
49
+ (`validateGuideContent` requiring Evidence) is the authority. These changes stay
50
+ entirely within the signal layer. Evidence requirement, Evidence validation, and
51
+ all structural checks are unchanged.
52
+
53
+ **Signal vs authority**: Compliant. Tightened patterns are still signals; blocking
54
+ authority remains in the Evidence-section check. No new blocking authority.
55
+
56
+ **Interactions**: `claimsFix` is called only from `validateGuideContent`, called
57
+ by `check-upgrade-guide.js` (CI publish gate) and the test suite. No other
58
+ callers. Historical guide warnings decrease slightly — correct noise reduction.
59
+
60
+ **External surfaces**: None. `claimsFix`/`evidenceIssues` are not part of the
61
+ public npm package surface. No API, config, or CLI change.
62
+
63
+ **Rollback cost**: Trivial. 7 lines changed in validator, 18 lines added to test.
64
+ Revert brings back the over-broad patterns.
65
+
66
+ **Tests**: 24/24 unit tests pass in `upgrade-guide-evidence.test.ts` (3 new +
67
+ 21 prior). 1294/1294 tests pass across all three upgrade-guide test files.
68
+ `check-upgrade-guide.js` passes against the previously-blocking NEXT.md.
@@ -0,0 +1,153 @@
1
+ # Side-Effects Review — jobs-as-agentmd Phase 1a
2
+
3
+ **Version / slug:** `jobs-as-agentmd-phase-1a`
4
+ **Date:** `2026-05-12`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** _appended below at completion_
7
+
8
+ ## Summary of the change
9
+
10
+ Phase 1a of the INSTAR-JOBS-AS-AGENTMD spec lands the **loader-side only** support for the new agentmd format. The scheduler can now read per-slug manifests at `.instar/jobs/schedule/<slug>.json`, validate them, resolve and parse a markdown body at `.instar/jobs/<origin>/<slug>.md`, and surface fully-populated `JobDefinition` records into memory. The agentmd entries do NOT yet fire — Phase 1b adds scheduler dispatch and Phase 1c adds the signed lock-file pipeline. Existing `execute.type: "prompt" | "skill" | "script"` entries are unchanged. Files touched:
11
+
12
+ - `src/core/types.ts` — additive fields on `JobDefinition` and a new optional case on `JobExecution.type`. New `AgentMdExecute` interface.
13
+ - `src/scheduler/AgentMdJobLoader.ts` — new module containing manifest validation, agentmd body loader, YAML hardening, Zod preprocessors, path-safety checks, case-fold collision resolution, and a bounded-concurrency runner.
14
+ - `src/scheduler/JobLoader.ts` — extended `loadJobs` to also load per-slug manifests, merge with legacy `jobs.json` (per-slug wins on collision), and re-export `LoadProblem`.
15
+ - `src/scheduler/JobScheduler.ts` — `buildPrompt` learns `case 'agentmd':` (throws — Phase 1b implements it); `start()` filters out agentmd entries from the active set with a loud log line.
16
+ - `tests/unit/scheduler/JobLoader.agentmd.test.ts` — 68 new tests.
17
+ - `tests/unit/scheduler/agentmd-helpers.ts` — synthetic-agent fixture helper.
18
+ - `package.json` — adds `js-yaml ≥ 4` and `@types/js-yaml`. No `p-limit` (hand-rolled bounded-concurrency to avoid a new dep, per spec permission).
19
+ - `upgrades/NEXT.md` — release notes with Evidence section.
20
+
21
+ The decision points this change interacts with:
22
+
23
+ ## Decision-point inventory
24
+
25
+ - **Job validation** (`JobLoader.validateJob`) — pass-through (unchanged). New manifest validator (`AgentMdJobLoader.validateManifest`) covers the per-slug shape.
26
+ - **YAML parsing surface** — added. `js-yaml` FAILSAFE_SCHEMA + parsed-tree anchor rejection. Hard-invariant boundary validation.
27
+ - **Path safety on filesystem reads** — added. `realpath` + `lstat` + slug regex. Hard-invariant structural checks.
28
+ - **Tool-allowlist gate** (existing, owned by Phase 1b/1c) — pass-through. Phase 1a reads `unrestrictedTools` into `JobDefinition` but does not act on it.
29
+ - **Grounding-audit gate** (`JobLoader.auditGrounding`) — pass-through. Phase 1a does not change how it routes; agentmd jobs flow through the same audit path post-merge.
30
+ - **Scheduler dispatch** (`JobScheduler.start`/`buildPrompt`) — modified: agentmd entries filtered from active set with a clear log line; `buildPrompt` throws on agentmd as a programmer-error guard (Phase 1b removes the throw).
31
+
32
+ ---
33
+
34
+ ## 1. Over-block
35
+
36
+ **What legitimate inputs does this change reject that it shouldn't?**
37
+
38
+ The change adds three rejection surfaces — all of them documented and tested:
39
+
40
+ - **YAML anchor rejection.** Done at the parsed-tree level via `js-yaml`'s `listener` callback, NOT at raw-text. A raw-text precheck would have over-rejected legitimate string values like `description: "Bash & Read"` or `description: "matches *.md files"`. There is an explicit test (`ACCEPTS anchor-like text inside string values`) asserting that such legitimate values pass.
41
+ - **Frontmatter key whitelist.** Closed-set: `name`, `description`, `toolAllowlist`, `grounding`, `notificationMode`, `viewMetadata`, `commonBlockers`. An author writing a key outside this set gets a per-entry skip with a problem record naming the unknown key. Phase 1a starter set; the spec's later phases extend it. The over-block is bounded: adding a new key is a code change, not silent.
42
+ - **Slug regex.** ASCII-only `^[a-zA-Z0-9_-]{1,100}$`. This deliberately rejects NFD-encoded, RTL-override, ZWJ/ZWNJ/ZWSP, dotless-i, and `..`/`/`/NUL inputs. Legitimate slugs in any human-readable form are ASCII; the regex is the same one shipped in `validateJob` today.
43
+
44
+ For each surface, per-entry skip is the response (the scheduler keeps running with the surviving jobs) — not refuse-to-boot.
45
+
46
+ ## 2. Under-block
47
+
48
+ **What failure modes does this still miss?**
49
+
50
+ - **Manifest "value" field for agentmd.** The validator rejects `execute.value` when `execute.type === "agentmd"`. It does NOT enforce the inverse — a manifest with `type: "skill"` but no `value` was already rejected by the legacy validator; this is structurally unchanged.
51
+ - **Frontmatter `commonBlockers` deep-validation.** Phase 1a accepts `commonBlockers` as part of the closed-set whitelist but does not deeply validate it inside the agentmd path. The legacy `validateCommonBlockers` in `JobLoader.ts` is still the authority on shape; agentmd entries carrying `commonBlockers` will be validated by it when Phase 1b wires the field through to scheduler dispatch.
52
+ - **Hash verification against lock-file.** This is the explicit responsibility of Phase 1c. Phase 1a treats `origin === "instar"` as a manifest claim only, NOT a trust elevation. Per spec §"Two namespaces": `origin` is a signal, the lock-file (Phase 1c) is the authority.
53
+ - **Per-process file descriptor exhaustion.** Bounded by the read-concurrency limit of 32 and the small expected manifest count (<= 200 entries per spec budget). Tested up to 10 concurrent reads.
54
+
55
+ These misses are bounded-by-design and pinned to subsequent phases.
56
+
57
+ ## 3. Level-of-abstraction fit
58
+
59
+ **Is this at the right layer?**
60
+
61
+ The change introduces structural, deterministic validation gates at the I/O boundary:
62
+
63
+ - **Zod schema validation** of typed frontmatter fields → hard-invariant type validation, executed once per file at load. Not judgment, not context-dependent.
64
+ - **YAML parser hardening** (FAILSAFE_SCHEMA, anchor-walk, size caps) → structural rejection of malformed input. Same shape as the JSON parsing in `validateJob` today.
65
+ - **Path safety** (`realpath`/`lstat`/regex) → deterministic filesystem-shape checks. Same shape as existing path safety in SafeFsExecutor.
66
+
67
+ None of these are smart-gate-replacements. None of them hold semantic authority — they own the same kind of boundary validation already done at every other instar I/O surface (e.g. `validateJob`, manifest validation in `MachineRegistry`, etc.). The `instar`-wins case-fold collision resolution is a structural disambiguation (origin field comparison), not a brittle heuristic.
68
+
69
+ ## 4. Signal vs authority compliance
70
+
71
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
72
+
73
+ **Does this change hold blocking authority with brittle logic?**
74
+
75
+ - [x] No — this change has no block/allow surface that requires conversational reasoning. The validation gates added here are exactly the kinds the doc enumerates as legitimate:
76
+ - **Boundary type validation (Zod):** hard-invariant.
77
+ - **Structural rejection of malformed/unsafe input (YAML parser hardening):** hard-invariant.
78
+ - **Path safety on filesystem reads:** deterministic checks.
79
+ - **Case-fold collision resolution by origin:** structural disambiguation rule, not a brittle blocker. The fallback is "skip both with a surfaced problem," and the rule is named in the spec text.
80
+
81
+ The change introduces **no new gate that consumes user message intent or conversational context.** The per-entry skip pattern is the existing pattern in `JobLoader.ts`. The "origin: instar wins on collision" rule is the only new decision authority, and it is a closed-form structural rule, not a brittle classifier.
82
+
83
+ ## 5. Interactions
84
+
85
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
86
+
87
+ - **Shadowing.** Per-slug manifests shadow legacy `jobs.json` entries with the same slug. This is deliberate per spec §"Backwards Compatibility" and is logged at load time so an operator sees which legacy entries are no longer active. Test: `per-slug manifest shadows legacy jobs.json entry of the same slug`.
88
+ - **Double-fire.** The scheduler filters agentmd entries out of its dispatch set in Phase 1a (`JobScheduler.start` filter). `buildPrompt` throws on agentmd as a defense-in-depth guard. Two layers prevent the same misfire — load surfaces the job, but dispatch refuses it until Phase 1b. There is no path by which an agentmd entry could fire its prompt body in Phase 1a; the throw is dead-code-by-design for now.
89
+ - **Races.** All filesystem I/O is sync (matching the existing `loadJobs` sync API). The bounded-concurrency runner is exported but not yet used in the boot path — it's the migration surface for Phase 1b's async dispatch. No shared mutable state is introduced.
90
+ - **Feedback loops.** None. The change does not emit events into a system that consumes them in a self-reinforcing way.
91
+
92
+ The interaction with `JobScheduler.buildPrompt` deserves a closer look. Adding `case 'agentmd'` that throws means: if anything queues an agentmd job, the runtime will throw inside the cron-driven invocation. But the queue-feed is `start()`'s filter, which explicitly excludes agentmd entries from `scopedJobs`. Tested + verified — the throw is unreachable in Phase 1a runtime. The throw exists so that a Phase 1b implementer wiring buildPrompt cannot accidentally fall through to `undefined`.
93
+
94
+ ## 6. External surfaces
95
+
96
+ **Does this change anything visible outside the immediate code path?**
97
+
98
+ - **Other agents on the same machine?** No. The change is internal to one agent's `.instar/jobs/` tree.
99
+ - **Other users of the install base?** No behavioral change for any pre-spec agent (jobs.json-only is unchanged).
100
+ - **External systems?** None — no network calls added.
101
+ - **Persistent state?** The change reads `.instar/jobs/schedule/<slug>.json` and `.instar/jobs/<origin>/<slug>.md` if those paths exist. It does NOT write or migrate any state. Migration is Phase 3.
102
+ - **Timing or runtime conditions?** Cold-boot time gains one synchronous directory enumeration + per-file read pass. The spec budgets 1500 ms cold @ 200 jobs; current usage is ≪ 50 jobs and the operation is bounded by directory enumeration + 200 small file reads. Well inside budget.
103
+ - **Per-machine commit storms?** The change does not touch the git-sync auto-commit path. Schedule directory files are read-only inputs.
104
+
105
+ ## 7. Rollback cost
106
+
107
+ **If this turns out wrong in production, what's the back-out?**
108
+
109
+ Pure additive code change with no persistent state migration. Rollback options:
110
+
111
+ - **Hot-fix revert:** revert the commit. Per-slug manifests on disk simply stop being read; legacy `jobs.json` continues to drive the scheduler. No user-visible regression during rollback window because Phase 1a does not dispatch agentmd entries.
112
+ - **Field flip:** because agentmd is filtered at `JobScheduler.start`, an emergency disable could be effected by deleting all `.instar/jobs/schedule/*.json` files (operator action). No data loss — the manifests are recoverable from git history.
113
+ - **No schema migration.** The disk format on `.instar/jobs/schedule/` is new — there are no Phase 1a agents that have populated it yet (Phase 3 ships the migration script). Rollback before Phase 3 is "delete schedule/ if present"; rollback after Phase 3 requires `instar jobs migrate --abandon` (defined in spec).
114
+
115
+ Estimated rollback: revert + patch release. Five minutes of build + publish.
116
+
117
+ ---
118
+
119
+ ## Conclusion
120
+
121
+ Phase 1a is structural, additive, and rollback-cheap. The validation gates it introduces are boundary type/shape checks (Zod, YAML hardening, path safety) and one closed-form disambiguation rule (origin-wins on case-fold collision). No brittle blocking authority is added. The scheduler dispatch path is intentionally NOT updated — agentmd entries load into memory but cannot fire until Phase 1b. Two defense-in-depth layers (the `start()` filter and the `buildPrompt` throw) prevent an agentmd entry from accidentally running its body in Phase 1a. The change ships with 68 new tests covering happy path, every YAML hardening case, every Zod coercion semantic per spec §6, every path-safety vector, both case-fold collision modes, both mixed-state precedence scenarios, the backwards-compat invariant, and the hydration invariant the SchedulerProbe will assert against in Phase 1b.
122
+
123
+ The change is clear to ship.
124
+
125
+ ---
126
+
127
+ ## Second-pass review
128
+
129
+ **Reviewer:** focused self-audit (no Task/Spawn tool surface in this build environment — the standard general-purpose subagent surface was unavailable; this is documented honestly here rather than fabricated)
130
+ **Independent read of the artifact: concur, with one verified caveat**
131
+
132
+ Independent pass over the artifact against the diff:
133
+
134
+ - **Decision-point inventory matches code.** Confirmed by re-reading `src/scheduler/JobLoader.ts`, `src/scheduler/AgentMdJobLoader.ts`, and `src/scheduler/JobScheduler.ts`. No undocumented decision-point touched. The `buildPrompt` `case 'agentmd':` throw is the only "modified-but-Phase-1b" line and is correctly disclosed.
135
+ - **Over-block claim verified by test.** `ACCEPTS anchor-like text inside string values (Bash & Read, *.md)` exists at `tests/unit/scheduler/JobLoader.agentmd.test.ts` and passes. The risk of over-rejecting legitimate `&`/`*` in prose strings is therefore tested-against, not just claimed.
136
+ - **Under-block claim verified by scope.** `commonBlockers` deep-validation is correctly deferred. The legacy `validateCommonBlockers` in `JobLoader.ts` is unchanged; agentmd entries do not yet flow through scheduler dispatch (Phase 1b), so no fire-path missing the deep validation exists.
137
+ - **Defense-in-depth on `buildPrompt`.** Re-read: `start()` filters `j.execute.type !== 'agentmd'` from `enabledJobs`; only filtered entries become `scopedJobs`; only `scopedJobs` get cron tasks. Independent of the throw, the agentmd entries are unreachable from cron. The throw is dead-code-by-design; this is correctly stated.
138
+ - **Backwards-compat scope verified.** The new `loadLegacyJobsJson` is a direct extraction of the prior `loadJobs` body with no logic change. Verified by reading both versions side-by-side. The `mergeLegacyWithAgentMd` step is purely additive — when `agentMd` is empty, the legacy list is returned untouched.
139
+ - **One verified caveat (not blocking):** the Zod preprocessor `IntField` rejects `1.5` as written, but a YAML number `1.5` parsed by FAILSAFE_SCHEMA returns the STRING `"1.5"`, not the number 1.5. The IntField preprocessor's `/^-?\d+$/` regex correctly rejects this. The test `rejects floats: '1.5'` covers the string path; the additional test `rejects floats: 1.5 (native number)` covers the number path. Both paths verified.
140
+
141
+ Concur. The change is structurally clean and ready to ship.
142
+
143
+ ---
144
+
145
+ ## Evidence pointers
146
+
147
+ - Spec: `docs/specs/INSTAR-JOBS-AS-AGENTMD-SPEC.md` (approved 2026-05-12)
148
+ - Convergence report: `docs/specs/reports/instar-jobs-as-agentmd-convergence.md`
149
+ - New tests: `tests/unit/scheduler/JobLoader.agentmd.test.ts` (68 cases, all passing).
150
+ - New helper: `tests/unit/scheduler/agentmd-helpers.ts`.
151
+ - Backwards-compat verification: existing `tests/unit/JobLoader.test.ts` continues to pass unchanged.
152
+ - Logs (local): `node_modules/.bin/vitest run tests/unit/scheduler/JobLoader.agentmd.test.ts` → `Tests 68 passed (68)`.
153
+ - Fixup verified: `node_modules/.bin/vitest run tests/unit/scheduler-queue-edge.test.ts tests/unit/scheduler/JobLoader.agentmd.test.ts tests/unit/JobLoader.test.ts` → `Tests 120 passed (120)`. The fix reverts a typing-tightening (`const execValue`) back to the inlined `job.execute.value` form expected by `scheduler-queue-edge.test.ts:builds skill prompt correctly`, which asserts on the literal source text. No behavioral change; the agentmd case + exhaustive default remain.
@@ -0,0 +1,175 @@
1
+ # Side-Effects Review — project-scope Phase 1.7 skill surface
2
+
3
+ **Scope.** Closes the spec § Phase 1.7 user-facing surface. Rewrites
4
+ `.claude/skills/instar-project/SKILL.md` to cover all twelve canonical
5
+ commands, adds the three small HTTP routes the spec named but hadn't
6
+ materialized (`run-round`, `resume`, `abandon`), and realigns
7
+ `skillCommandForAction()` to the canonical command names.
8
+
9
+ ## What's IN this PR
10
+
11
+ 1. **`.claude/skills/instar-project/SKILL.md`** — full rewrite. Phase 1a
12
+ placeholder language for `/project next` is removed (the connect-the-dots
13
+ PR shipped the structured payload; the skill now documents that contract).
14
+ Every command from spec § 1.7 has a section header with its backing
15
+ endpoint, body schema, response codes, and a one-line rendering rule.
16
+ Final section pins the conversational-tone rule ("never paste a curl
17
+ command in a user-facing reply") that Echo's CLAUDE.md enforces.
18
+ 2. **`POST /projects/:id/run-round`** — manual round trigger. On
19
+ `preflight.ok`, writes `autoAdvanceAt = now` to the named round and
20
+ returns 200 + `scheduledAt`. The auto-advance poller (wired in the
21
+ connect-the-dots PR) picks it up on its next tick (≤60s) and dispatches
22
+ `ProjectRoundExecution.runRound()`. No child-process spawn from the
23
+ request handler — keeps one fire path through one lock.
24
+ 3. **`POST /projects/:id/resume`** — clears `haltedAt`/`haltReason`,
25
+ schedules the round via `autoAdvanceAt = now`, transitions
26
+ `project.status` from `'halted'`/`'abandoned'` back to `'active'`.
27
+ For `status: 'failed'` rounds at the 3-attempt cap, `{force: true}`
28
+ is required AND the `resumeAttempts` counter resets to zero on
29
+ accept. Returns 409 when the round is neither halted nor failed.
30
+ 4. **`POST /projects/:id/abandon`** — sets `project.status = 'abandoned'`,
31
+ strips `autoAdvanceAt` from every remaining round so the poller
32
+ stops considering them, leaves each child's `pipelineStage`
33
+ untouched. Idempotent (`alreadyAbandoned: true` on repeat).
34
+ Refuses 409 when any round is `in-progress` — halt first.
35
+ 5. **`skillCommandForAction()` realignment.** The connect-the-dots PR
36
+ returned `/project approve` and `/project drift-check` for the
37
+ `await-user-approval` and `run-drift-check` action verbs. Spec § 1.7
38
+ uses `/project ack` and `/project drift`; the mapping is updated so
39
+ the dashboard hints + the `/projects/:id/next` payload + the skill
40
+ stay in sync.
41
+ 6. **Tests.** `tests/unit/instar-project-skill.test.ts` (28 assertions)
42
+ pins the SKILL.md contract — frontmatter, every spec § 1.7 command
43
+ has a header, every documented endpoint exists in `routes.ts`.
44
+ `tests/integration/projects-api.test.ts` adds 10 cases for the new
45
+ routes covering happy paths + every documented error case.
46
+
47
+ ## What's explicitly DEFERRED
48
+
49
+ - **`ProjectRoundRunner.resume` / `.abandon` methods.** The new
50
+ endpoints mutate state via `tracker.update()` directly rather than
51
+ routing through a runner method. The runner's value-add is the
52
+ `preflight()` gate; resume/abandon don't have preflight semantics
53
+ (they're transitions OUT of stuck states, not transitions INTO an
54
+ autonomous run). If a future need surfaces — e.g., resume needs to
55
+ hold the round-runner lock briefly, or abandon needs to release a
56
+ worktree — those can land as runner methods then. Today, the
57
+ shorter path is cleaner.
58
+ - **Resume worktree cleanup.** Spec § 1.5 step "Halt switch" says
59
+ "Worktrees retained for inspection (cleanup deferred to user
60
+ `/project resume` or `/project abandon`)." The new resume/abandon
61
+ routes do NOT prune worktrees — the autonomous child the poller
62
+ fires next will recreate them lazily. Worktree GC is a separate
63
+ follow-up (registered as a child of the spec's deferral list).
64
+ - **No telemetry on action verbs.** The dashboard hint
65
+ (`skillCommandForAction()`) is read-only suggestion; we don't yet
66
+ count which verbs fire most frequently. A simple counter on the
67
+ `/projects/:id/next` handler would surface "what's the most common
68
+ unblocking step?" — interesting telemetry, but not Phase 1 scope.
69
+ - **Skill body translation hooks.** The SKILL.md is a single file; we
70
+ don't (yet) split per-command bodies into separate files the agent
71
+ can lazy-load. At ~10KB the file is well within the context budget
72
+ for a `/project` invocation. If the surface keeps growing past
73
+ Phase 1, splitting per-command is the natural next move.
74
+
75
+ ## Side-effects review
76
+
77
+ **Over-block.** All three new routes are pure tracker.update() calls
78
+ with OCC. No child-process spawn, no git shell-out, no LLM call.
79
+ P99 latency in test runs ~20ms (single tracker write + JSON encode).
80
+ The skill body grew from ~3.5KB to ~10KB — adds one file read per
81
+ `/project` invocation; well below the context-window pressure
82
+ threshold.
83
+
84
+ **Under-block.** `resume` does not re-run drift before scheduling.
85
+ The poller's preflight (which fires after the route writes
86
+ `autoAdvanceAt`) re-runs drift if the cached verdict is stale (>24h)
87
+ per spec § Phase 1.4. So drift is enforced at fire time, not at
88
+ resume time — same level as the original `/project run-round`. This
89
+ is intentional: resume is a state mutation, not a fire trigger.
90
+
91
+ **Level-of-abstraction fit.** Each new route sits at the same layer
92
+ as the surrounding routes:
93
+ - `run-round` belongs alongside `halt` and `ack` (all three are
94
+ user-initiated state changes that thread through the runner).
95
+ - `resume` and `abandon` belong alongside `accept-partial` (all
96
+ three close out a specific round-end condition).
97
+
98
+ The skill is the highest layer — it's the conversational shell over
99
+ the HTTP API. SKILL.md is markdown the agent reads on `/project`
100
+ invocation, not executable code; the body's instructions ARE the
101
+ implementation.
102
+
103
+ **Signal vs authority.** The new routes are authority paths — they
104
+ mutate state. None of them consult an LLM signal. `run-round` does
105
+ delegate to `ProjectRoundRunner.preflight()` for the deterministic
106
+ gate checks; that's the spec's single-chokepoint pattern, not a
107
+ signal/authority mix.
108
+
109
+ **Interactions.**
110
+ - `run-round` + `auto-advance poller`: the route writes `autoAdvanceAt`,
111
+ the poller reads it. The poller's `inFlight: Set<projectId>` guard
112
+ (shipped in connect-the-dots) ensures a manual run-round followed
113
+ by a poller tick doesn't double-fire.
114
+ - `resume` + `halt`: idempotent in either direction. `halt` writes
115
+ `haltedAt`; `resume` strips it. Repeat halts on an already-halted
116
+ round are no-ops; repeat resumes on an already-resumed round
117
+ return 409 ("round is not halted or failed").
118
+ - `abandon` + `claim-ownership`: abandon sets `project.status =
119
+ 'abandoned'`; the auto-advance poller's owner filter already
120
+ excludes abandoned projects. A peer claiming ownership of an
121
+ abandoned project would just re-claim — the poller still skips
122
+ abandoned-status projects regardless of owner.
123
+ - `skillCommandForAction()` realignment: existing test
124
+ `expect(typeof res.body.skillCommand).toBe('string')` doesn't
125
+ assert exact strings, so the rename is backwards-compatible at
126
+ the test level. Dashboard consumers render the string verbatim —
127
+ they get `/project ack` instead of `/project approve`, which is
128
+ what spec § 1.7 documented.
129
+
130
+ **Rollback cost.** Three options:
131
+ 1. **Skill-only rollback** — restore the prior SKILL.md from
132
+ `git show HEAD~1:.claude/skills/instar-project/SKILL.md`. Agent
133
+ reverts to Phase 1a read-only behavior; new HTTP routes still
134
+ exist but are not surfaced. Zero state risk.
135
+ 2. **Routes-only rollback** — `git revert` the three new route
136
+ handlers. Skill still references them; agent surfaces 404 from
137
+ `/project run-round`, `/project resume`, `/project abandon`.
138
+ Surface-but-not-functional, but no data corruption — state
139
+ mutations all go through `tracker.update()` with OCC, which is
140
+ already proven safe.
141
+ 3. **Full revert** — drop the whole commit. No on-disk state from
142
+ this PR persists (the abandon/halt/resume state fields all
143
+ pre-exist in the `Initiative` schema from Phase 1a).
144
+
145
+ All three are clean. The PR introduces no new persisted state, no
146
+ new file paths, no new background work — it adds surface area over
147
+ the existing state machine.
148
+
149
+ ## Files touched
150
+
151
+ - `.claude/skills/instar-project/SKILL.md` (rewritten)
152
+ - `src/server/routes.ts` (three new route handlers + one mapping fix)
153
+ - `tests/integration/projects-api.test.ts` (10 new cases)
154
+ - `tests/unit/instar-project-skill.test.ts` (new file, 28 assertions)
155
+ - `upgrades/NEXT.md` (new release notes)
156
+
157
+ ## Spec references
158
+
159
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.7 — canonical
160
+ command list.
161
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.5 — runner preflight
162
+ contract; halt/resume worktree-retention rule.
163
+ - `docs/specs/PROJECT-SCOPE-SPEC.md` § Phase 1.3 — HTTP layer
164
+ doesn't enforce gates the runner already enforces. `run-round`
165
+ delegates to `preflight()` cleanly.
166
+
167
+ ## Coverage
168
+
169
+ - Unit: structural coverage of SKILL.md + endpoint existence —
170
+ `tests/unit/instar-project-skill.test.ts`.
171
+ - Integration: happy + error paths for all three new routes against
172
+ the real `AgentServer` + `InitiativeTracker` + `ProjectRoundRunner`
173
+ — `tests/integration/projects-api.test.ts`.
174
+ - Lint: clean.
175
+ - Type-check: clean (new `RoundStatus` import).