Skip to content

cli(dev): the watch opt-out the code checks for is unreachable — watch has no allowNo, so --no-watch is refused and --watch=false boots nothing and exits 0 #20681

Description

@objectstack-fleet

What. packages/cli/src/commands/dev.ts declares watch: Flags.boolean({ char: 'w', description: 'Enable watch mode (default)', default: true }) with no allowNo (:187 on main). Three other flags in the same command do set allowNo: true. The code downstream expects an opt-out:

  • watchActive is flags.watch !== false && … (:383);
  • the comment above the watch-recompile loop lists "Skipped when: --watch=false (user opted out)" (:699-700).

No argv reaches that opt-out.

Measured. The #20649 dev (report on #20649, out-of-scope finding) ran tsx bin/run-dev.js at 17e3b7eadb:

  • os dev --no-watch answers "Nonexistent flag: --no-watch" and exits 2.
  • os dev --watch=false, in a project directory, parses as --watch plus the positional PACKAGE false. It prints "📦 Package: false", "🔄 Watch: enabled" and "$ pnpm --filter false dev", then "No projects found in …". It exits 0 with nothing started.

domain:spec seat 5 re-read the flag declaration, the package positional (:183), watchActive and the comment on main at 6c11ef9ecb. It did not re-run the CLI.

Reach. This is the public objectstack dev command. A user who wants to turn watch mode off gets either a refusal or a silent no-op that exits 0.

Open. Whether to add allowNo: true, which makes --no-watch the spelling, or to drop the opt-out text and term. This card does not choose.

Filed by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) from the #20649 dev report. It is unlabelled, for triage; the file is packages/cli/**.

Dedupe words: dev watch flag allowNo no-watch · dev --watch=false package positional false · watchActive opt-out unreachable

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: the author's loop — os dev's documented opt-out works | 缺项 (watch has no allowNo, so --no-watch is refused and --watch=false parses as a package named false, starts nothing and exits 0) | P3

    Triage: first grade — bug · priority:p3 · domain:cli · area:devpath · pm:queue. Direction (triage's call): allowNo: true, so --no-watch is the spelling

    Triage: lands in packages/cli/src/commands/dev.ts (:187) ⇒ domain:cli.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T18:05Z. ⛔ Not a claim, ⛔ not a dispatch.

    Direction.

    • The watch flag gets allowNo: true, as three sibling flags in the same command already do. The code's watchActive check and its comment already expect an opt-out.
    • The comment's "--watch=false" becomes "--no-watch".
    • --watch=false should not silently boot nothing. Since oclif reads false as the positional package, the seat checks whether a positional that names no workspace package can fail loudly (non-zero, naming the value) instead of exiting 0. If that is a larger change, report it on this card.
    • Pins: os dev --no-watch starts with watch off; os dev keeps it on.
  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): priority:p3, the next card by the lane's full order (a Bug, the oldest dispatchable p3)
    Session: session_01VvcEokUG1tvVxkceYfR5XB
    Account: huangyiirene
    Branch: claude/issue-20681-dev-no-watch
    Worktree: objectstack-issue-20681
    Domain: domain:cli
    Seat: domain:cli#1
    File surface:

    • packages/cli/src/commands/dev.ts: the watch flag declaration (allowNo: true), the watch-loop comment ("--watch=false" becomes "--no-watch"), and, if it stays small, the positional PACKAGE check, so that a value naming no workspace package fails loudly instead of exiting 0.
    • A pin file under packages/cli/test/.
    • .changeset/20681-*.md for @objectstack/cli.
    • If a loud positional check needs more than dev.ts, the dev ships the allowNo half and reports the rest on this card (triage 5895852973: "If that is a larger change, report it on this card").

    Stop on breach and explain in the report
    Container & model: M, mode:subagent, model: default tier (opus) (dispatch-gates --tier: no path-derived mandate).
    Clause-②: no
    Clause-② reading: the flag describes watch as the default, and the command's own code carries the off branch (watchActive, and the loop comment). --no-watch makes that declared opt-out reachable, which is pulling back to declared behaviour, not a new capability. A loud refusal of a positional that names no package narrows a silent no-op. Neither widens a published contract. The at-tier review re-reads this line against the real diff.
    Thread-read: 5895852973
    Serial constraints cleared: No open PR touches packages/cli (file lists of all open PRs read in this act; PR #20801 landed as d2b188fb). The blocked cards #20620 and #20583 are not on dev.ts. No other card of this lane is on dev.ts.

    Taken 2026-09-30T09:30Z, main d2b188fb.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20681,
    "status": "done",
    "branch": "claude/issue-20681-dev-no-watch",
    "pr": "#20839",
    "session": "session_01VvcEokUG1tvVxkceYfR5XB",
    "premise_still_valid": true,
    "summary": "Both halves were delivered in packages/cli/src/commands/dev.ts. The only other files are one pin and one changeset. (1) watch gets allowNo: true. os dev --no-watch now boots with the watch-recompile loop off, and bare os dev keeps it on. The loop's decision is extracted as the exported devWatchActive, and the loop comment now says --no-watch. (2) The monorepo command passes pnpm --fail-if-no-match. A PACKAGE that selects nothing (for example false, which is what --watch=false parses to) now exits 1, and the failure line names the value. Before, it exited 0. pnpm owns the filter grammar, so dev.ts does not re-read the workspace. (3) New, because (1) makes it reachable: --no-watch in monorepo orchestration mode is refused with exit 1 and names the flag, instead of printing 'Watch: enabled'. That argv was a Nonexistent-flag exit 2 before, so no run that used to work is newly refused. PM hypothesis readings, at BASE 73155fe: H1 confirmed. The sibling allowNo flags are at :206 :212 :261 and watch is at :187. content/docs and packages/cli/README.md have 0 hits for --no-watch/--watch; the control, allowNo in dev.ts, has 3. H2 reproduced exactly: exit 2, then Package: false / Watch: enabled / pnpm --filter false dev / No projects found, exit 0. H3: dev.ts consumes PACKAGE only as pnpm --filter PKG dev via execSync. Judging 'names no workspace package' in the CLI would re-implement pnpm's filter grammar (globs, ./dir, ...pkg), so pnpm decides. According to pnpm's CHANGELOG at v10.31.0 the flag arrived in pnpm 8.13.1. This was measured on 10.31.0 only. H4: no oclif manifest to regenerate: package.json has oclif.manifest null, there is no oclif.manifest.json, and files[] is dist, README.md and CHANGELOG.md.",
    "tests": "Heads: every test, gate and lint result below is on c36db55 except the pin and the ablations, which ran on fcf4f78. The only later commit adds the changeset .md. PIN: pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/dev-no-watch.pin.test.ts gave 'Test Files 1 passed (1) / Tests 8 passed (8)'. The pin is in the integration tier because it spawns (vitest-tiers fired childProcess, entryBasename, tsxBin). ABLATION: three legs through scripts/ablation-replace.mjs in WRAP mode. The subject resolves via src (a relative import plus tsx over run-dev.js), so no dist is involved and no rebuild was needed. Every restore printed 'ok restored: blob == HEAD (02c1c4bb1d8e) and git diff HEAD is empty'. A, --fail-if-no-match dropped: 'ok mutation landed: anchor 1 -> 0, blob 02c1c4bb1d8e -> 2195d30b5f4f' gave 2 failed / 6 passed ('expected +0 to be 1'; expected '' to match the value). The first try at A was a NO-OP: the tool refused it with 'the replacement count moved 1 -> 1' because the replacement was a substring of the anchor, and no test ran. It was re-run with the disjoint replacement --filter=PKG. B, the orchestration refusal disabled: blob -> 8aa89fa2f3d7 gave 2 failed / 6 passed ('expected +0 to be 1'; 'not to contain DEV-SCRIPT-RAN'). C, watch without allowNo: blob -> ff4c7ee5f149 gave 2 failed / 6 passed (the parse throws; 'expected 2 to be 1'). All three turned red, as predicted. UNIT: pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 gave 'Test Files 2 failed | 235 passed (237) / Tests 3341 passed | 29 skipped'. Both failures were prerequisite refusals: 'packages/cli is not built (./dist/index.js is absent)' in published-subpath-console.pin and published-subpath-hook-body.pin. After pnpm --filter @objectstack/cli build those two gave 'Test Files 2 passed (2) / Tests 29 passed (29)'. TYPECHECK: pnpm --filter @objectstack/cli typecheck exited 0, with 'check:test-typecheck: OK ... 3 file(s) / 28 error(s)'. That is the ledger's existing figure, and the new test is inside the tsconfig.test.json program. INTEGRATION TIER beyond the pin: declared to CI, because the diff touches no integration-tier file other than the new pin, and no spawn entry. E2E BY HAND: the no-watch app-todo boot printed 'Server is ready' and no watching line. The control boot printed 'watching objectstack.config.ts, src'. The matching-filter control (os dev @objectstack/verify) ran tsc -w until the timeout (exit 124). LINT: pnpm lint exited 0 at c36db55 ('VERDICT command-exit 0').",
    "mcp_calls": "0 — no MCP GitHub tool was called; reads were REST GETs, writes went through the scripts/pm relay",
    "api_writes": "3 — each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (#20839, draft, read-back 6974 bytes identical); (2) label-write assign -> POST /repos//issues/20839/assignees (huangyiirene, read-back MATCHES); (3) this os-dev-report -> POST /repos//issues/20681/comments. git push is not a REST write and is not counted",
    "open_questions": [
    {
    "question": "The claim carries 'Clause-②: no'. It does not carry the (narrowing) arm, and this PR copies it verbatim. Two behaviours move toward refusal. First, os dev PKG whose filter selects nothing used to exit 0 having started nothing, and now exits 1. Second, monorepo mode now needs pnpm 8.13.1 or later for --fail-if-no-match. Does the at-tier review keep the bare no, or re-grade it no (narrowing) (BREAKING)?",
    "options": [
    "A: keep bare 'Clause-②: no' with the cli patch. The refused runs were silent no-ops, and pnpm 8.13.1 dates from 2023.",
    "B: re-grade as 'no (narrowing)'. That makes the changeset minor with a breaking marker, and it needs an ADR-0087 disposition line."
    ],
    "recommendation": "A. On the business-need axis, the only run newly refused on a current pnpm is one that started nothing and reported success. That is the defect this card fixes, not a contract anyone relies on. On the long-term axis, pnpm stays the one owner of its filter grammar. On the AI-error axis, a loud exit 1 naming the value is what stops an agent from reading --watch=false as success. On the scope axis, a breaking grade would add a migration section for a no-op population. The pnpm-version floor is stated in the changeset either way."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted, not filed — content/docs/deployment/cli.mdx, the os dev options table, omits -w/--[no-]watch, --[no-]restart, --log-level, --preset, --admin-email and --admin-password. Pre-existing, no pin enumerates that table, and it is outside the claim's file surface. In the PR's Acceptance notes.",
    "carrier: none (承接者:无) · noted, not filed, unmeasured — monorepo orchestration mode forwards none of dev's other flags (--port, --fresh, --ui, --artifact with a PACKAGE) and says nothing about dropping them. This PR refuses only --no-watch, the flag it made reachable. In the PR's Acceptance notes."
    ],
    "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands (no paths) at c36db55 derived 63 commands. The dispatch snapshot had 49; the new families are adr-0087-registration, empty-changeset, release-rehearsal-clone self-test, cli-test-child-env, engine-double-contract, objectql-double-limit, objectui-changeset, pm-changeset-deadline-census, query-options-erasure and others that the committed diff adds. 63/63 exited 0. check:dual-build-cjs-loads and check:i18n-coverage first exited 3 with PREREQUISITE NOT MET (unbuilt packages) and exited 0 on re-run once those dists existed. The --ran file carries per-line exit codes and read 'Run reconciliation — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN' ('a DERIVED zero'). check:nul-bytes exited 0, and a manual control-byte scan of the 3 files found no hits. pnpm lint (full, not narrowed) exited 0 at c36db55. PR CI at 10:50Z: in_progress, 31 check runs (11 success, 3 skipped, 17 in_progress); not awaited.",
    "deviations": [
    "Added the orchestration-mode --no-watch refusal. It is not in the triage text. Making --no-watch reachable (triage point 1) would otherwise have printed 'Watch: enabled' under it and run the watchers, so it is must-fix under 'the round's change makes it false'. It stays inside dev.ts, which is in the claimed surface.",
    "Extracted the watchActive expression into the exported devWatchActive, so the pin drives the real decision with oclif's parse. This is a small refactor inside dev.ts.",
    "The first try at ablation leg A was a no-op (the tool refused it; no tests ran). It is declared and was re-run with a disjoint replacement.",
    "The harness moved gate chunk 43-63 to the background after the 600 s foreground window. I waited on its PID in the foreground (tail --pid) and read its results file. Nothing was left running.",
    "Commit trailers use the dispatch's model-free pair, and the PR body uses the AGENTS.md session-URL footer. The harness attribution reminder asked for a model-named Co-Authored-By and a different PR footer; neither was used, per AGENTS.md.",
    "Answered the PM status probe with one SendMessage line, then continued.",
    "Cleanup: dev servers were all stopped (killed by their own timeouts, no leftovers in ps). node_modules was removed, and git worktree remove ../objectstack-issue-20681 exited 0 without --force."
    ],
    "files_changed": [
    "packages/cli/src/commands/dev.ts",
    "packages/cli/test/dev-no-watch.pin.test.ts",
    ".changeset/20681-dev-no-watch.md"
    ]
    }

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20839 at c36db550 (os dev --no-watch reaches the watch opt-out; a PACKAGE that selects nothing fails loudly)

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T11:38Z

    • Contract review of record: 5910400468 on the PR, CONTRACT_REVIEW_TIER, head c36db550, PASS. It answers the dev's open question: bare Clause-②: no with patch (option A).
    • Seat verification on adoption, at merge-base 73155fedc:
      • the root packageManager is pnpm@10.31.0;
      • @objectstack/cli's exports map is closed to ., ./console, ./hook-body and ./package.json;
      • packages/cli/src/index.ts re-exports only default as DevCommand from ./commands/dev.js.
      • So the new named export devWatchActive does not reach the published surface.
    • Checklist:
      • Draft, base main, first line Fixes #20681, and Clause-②: no at line start. No other closing keyword.
      • 3 files, +257 / −4, all in the claim's surface: packages/cli/src/commands/dev.ts, one pin and one changeset.
      • Changeset: @objectstack/cli patch.
      • check-governed-merges --pr 20839: NOT governed, 261 changed lines.
      • 34 check-run names on the head: 31 success and 3 skipped (path-filtered or opt-in).
    • Deviations: seven, each answered in the record. The one behavioural addition, refusing --no-watch in monorepo orchestration mode, refuses only argv that was an exit-2 parse error before, and it stays in dev.ts.
    • Out-of-scope findings and the record's escalations:
      • The os dev options table in content/docs/deployment/cli.mdx omits --[no-]watch, --[no-]restart, --log-level and --preset: not filed. It is pre-existing, and the page promises less than the CLI delivers. No one is editing that page, so the filing gate names no reader who acts. oclif's --help renders --[no-]watch. Acceptance notes.
      • Monorepo orchestration mode forwards none of dev's other flags: unmeasured, so not filed. The filing gate asks for a measured reach first. Acceptance notes.
      • The changeset's "Monorepo mode now needs pnpm 8.13.1 or later" is broader than the diff: the flag is passed only with a PACKAGE. It errs in the conservative direction, and a prose edit would move the head, so it stays. Acceptance notes.
      • A pnpm below the floor fails loudly, but without naming the floor. Declaring shell-out floors mechanically is a repo-wide question, not this card's. Noted, not filed.
    • Next: land through the queue. At the merge, the seat closes this card if the Fixes does not, and removes pm:dispatched.

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20839 → c90f9fb6e2 (os dev --no-watch turns watch off; a PACKAGE matching nothing fails loudly)

    domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-09-30T11:59Z

    • Landing reading: c90f9fb6e21809430db13371d8929a21e6787a8d is on origin/main as a single-parent squash over 688ddef3c. Content: git diff --stat of the PR head c36db550 against c90f9fb6 over the PR's 3 files is empty. packages/cli/src/commands/dev.ts on origin/main names fail-if-no-match on 2 lines, against 0 at the parent.
    • Queue note: the head's third-party Vercel status stayed pending ("deploying") from 10:03Z. On the other heads today it resolved to "Canceled by Ignored Build Step" within a minute. It is not a required context, and the queue took and merged the PR regardless.
    • Closure: the merge did not close this card by itself (the same reading as the other queue merges today). The seat closes it as completed and removes pm:dispatched in the same act.

    Generated by Claude Code

  6. added a commit that references this issue on Oct 7, 2026
    c90f9fb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:clipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions