|
| 1 | +# Cycle 2 (wp2, Bug B #879) — star-prompt deferral bound (C4 care: consent surface) |
| 2 | + |
| 3 | +## Non-goals (consent invariant — a diff touching any of these is a C-gate FAIL) |
| 4 | + |
| 5 | +- `src/server/management/sidebar-routes.ts` — `403 agent_consent_required` and the |
| 6 | + `isAgentDriven() && !hasBrowserSessionEvidence(req)` shape stay byte-untouched. |
| 7 | +- The human interactive path: TTY gate, `ghAvailable()` gate, Yes/No selector, |
| 8 | + marker `.star-prompted` written BEFORE the question, `if (!yes) return;`. |
| 9 | +- `hasStarPromptRun()` semantics — `src/update/notify.ts:135` yield behavior unchanged. |
| 10 | +- An agent never answers, auto-dismisses, or stars; the `gh api -X PUT` instruction |
| 11 | + stays gated on an explicit user yes. |
| 12 | + |
| 13 | +## Root cause (code-verified in issue #879; no external claims — no search lane needed) |
| 14 | + |
| 15 | +1. Agent path leaves `.star-prompted` unwritten → every agent-driven start re-prints |
| 16 | + `printAgentDeferral()` (`src/cli/star-prompt.ts:139`). |
| 17 | +2. Deferral text + AGENTS.md demand repeat-forever relay ("at the top of your next |
| 18 | + reply, unchanged" / "Silence is not a No"). |
| 19 | +3. Agent PTYs pass the TTY gate → fires during routine edit/test cycles. |
| 20 | + |
| 21 | +## Design |
| 22 | + |
| 23 | +New deferral record `.star-deferred` in `getConfigDir()` holding |
| 24 | +`"<ISO> <version>"`. The agent path becomes: if the record is current, print |
| 25 | +nothing; otherwise print the deferral once and write the record. "Current" = |
| 26 | +same ocx version (never re-ask for a version already asked on) OR younger than |
| 27 | +7 days (bound while version is unreadable). Net effect: at most one agent-facing |
| 28 | +relay per version, and at most one per week across upgrades — instead of every |
| 29 | +start, forever. |
| 30 | + |
| 31 | +The relay text itself is bounded to a single relay: ask once in the reply |
| 32 | +following the start that printed it; an unanswered question is NOT repeated in |
| 33 | +later replies — the CLI re-arms on a later version. AGENTS.md changes in |
| 34 | +lockstep (the repeat-forever bullets are the other half of the bug). |
| 35 | + |
| 36 | +## Diff-level file map |
| 37 | + |
| 38 | +- MODIFY `src/cli/star-prompt.ts` |
| 39 | + - ADD `const DEFERRAL = ".star-deferred"` + `DEFERRAL_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000`. |
| 40 | + - ADD exported pure helper `isDeferralCurrent(record: string | null, version: string, now: number): boolean` |
| 41 | + (exported for tests; no I/O). Semantics (audit-pinned): parse `<ISO> <version>`; |
| 42 | + malformed/NaN → false; same version → true ONLY when version !== "?" (a "?" |
| 43 | + sticky match would suppress re-arm forever); age rule requires |
| 44 | + `0 <= age < DEFERRAL_MAX_AGE_MS` — negative age (future-dated/corrupt record) |
| 45 | + fails toward re-asking. |
| 46 | + - IMPORT `currentVersion` from `../update/index` (audit blocker 1: the claimed |
| 47 | + import cycle does not exist — update/index's closure never reaches |
| 48 | + cli/star-prompt; notify.ts importing both is a diamond, not a cycle. No local |
| 49 | + readOwnVersion duplication). |
| 50 | + - MODIFY `maybeShowStarPrompt()` agent branch: `if (isDeferralCurrent(readRecord, version, Date.now())) return;` |
| 51 | + then `printAgentDeferral()` + best-effort record write (`recordOwnedConfigPath` |
| 52 | + + `writeFileSync`, mirroring the marker write). |
| 53 | + - MODIFY `printAgentDeferral()` text: single-relay instructions. Keep verbatim: |
| 54 | + do-not-answer rule, `"Star ${REPO}? Yes / No"` naming, `gh api -X PUT` gated on |
| 55 | + explicit yes, answer-settles-it, the `<details>` fold, dim single visible line. |
| 56 | + Remove: "Silence is not an answer... top of your next reply, unchanged". |
| 57 | +- MODIFY `AGENTS.md` — user-consent section: keep the three Do-NOT bullets and |
| 58 | + "An answer settles it"; replace the "Do relay it / Silence is not a No" bullets |
| 59 | + with the single-relay rule and the re-arm-on-later-version note. |
| 60 | +- MODIFY the full docs surface carrying the same repeat-forever wording (audit |
| 61 | + blocker 2 — 10 files, 5 languages; keep each locale's surrounding text and the |
| 62 | + test-locked strings `agent_consent_required` / `never an agent`): |
| 63 | + `README.md`, `readme/README.ko.md`, `README.zh-CN.md`, `README.ja.md`, |
| 64 | + `README.ru.md`, and `docs-site/src/content/docs/**/getting-started/for-agents.md` |
| 65 | + (en/ko/zh-cn/ja/ru). Korean edits follow the repo Korean-prose rules (no |
| 66 | + translationese, one register). |
| 67 | +- MODIFY `tests/startup-prompt.test.ts` |
| 68 | + - Update the two wording-locked tests to the bounded text. KEEP or equivalently |
| 69 | + re-lock every existing consent guard (audit blocker 4): `/soft aside/`, |
| 70 | + "Ask the user, in your reply, whether to star", `"Star ${REPO}? Yes / No"`, |
| 71 | + gh-command assertion, fold structure, guard-before-marker order, |
| 72 | + `if (!yes) return;`, `not.toMatch(/declined/i)`, `not.toMatch(/remind the user/i)`. |
| 73 | + ADD positive locks for the new semantics: deferral text states a non-answer |
| 74 | + settles nothing (silence = deferred, never a Yes, never a recorded No) AND the |
| 75 | + re-arm rule (re-appears on a later version), instead of only deleting the three |
| 76 | + repeat-forever strings. |
| 77 | + - ADD source assertions: agent branch checks `.star-deferred` BEFORE |
| 78 | + `printAgentDeferral`; `.star-prompted` marker write still gated behind the agent |
| 79 | + guard (existing order assertion stays); `hasStarPromptRun` still reads only |
| 80 | + `.star-prompted`. |
| 81 | +- MODIFY `tests/agent-driven.test.ts` — no change expected (pure env detection); |
| 82 | + confirm green. |
| 83 | +- ADD runtime tests for `isDeferralCurrent` (in `tests/startup-prompt.test.ts` or a |
| 84 | + new `tests/star-deferral.test.ts`): null record → false; malformed → false; same |
| 85 | + version (non-"?") → true; version `"?"` + `"?"` record → age rule only; |
| 86 | + different version + 0 <= age < 7d → true; age > 7d → false; future-dated |
| 87 | + (negative age) → false. |
| 88 | + |
| 89 | +Note (audit, non-blocking): the agent branch becomes the first config-writing |
| 90 | +path in agent-driven runs (record write); acceptable — it writes only the |
| 91 | +deferral record, never the marker, and `recordOwnedConfigPath` covers uninstall |
| 92 | +cleanup dynamically. |
| 93 | + |
| 94 | +## Activation scenarios (C) |
| 95 | + |
| 96 | +1. Red: new `isDeferralCurrent` tests fail before the helper exists; updated |
| 97 | + wording tests fail while the old repeat-forever text is present. |
| 98 | +2. Green: all updated + new tests pass after the change. |
| 99 | +3. Invariant sweep: `tests/startup-prompt.test.ts` (management-endpoint refusal, |
| 100 | + human-prompt gates), `tests/agent-driven.test.ts`, `tests/sidebar-routes.test.ts` |
| 101 | + all green; `git diff --stat` shows NO change to `sidebar-routes.ts`, |
| 102 | + `interactive-confirm.ts`, `agent-driven.ts`. |
| 103 | +4. Full `bun run test` + `bun run typecheck`. |
0 commit comments