@afokapu/atdd-bun 0.10.27 → 0.10.29

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.
Files changed (25) hide show
  1. package/conventions/coder.bun/coder.bun.dead-code-reachability.convention.yaml +2 -1
  2. package/conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml +1 -0
  3. package/conventions/coder.bun/coder.bun.logging-console.convention.yaml +1 -1
  4. package/conventions/coder.bun/coder.bun.runtime-server-is-bun-serve.convention.yaml +1 -1
  5. package/conventions/coder.bun/coder.bun.runtime-single-lockfile.convention.yaml +2 -2
  6. package/conventions/delivery/delivery.board.convention.yaml +2 -2
  7. package/conventions/delivery/delivery.config-schema.convention.yaml +1 -1
  8. package/conventions/delivery/delivery.model-allowed.convention.yaml +5 -17
  9. package/conventions/delivery/delivery.operating-model.convention.yaml +2 -2
  10. package/conventions/tester.bun/tester.bun.test-imports-bun-test.convention.yaml +2 -2
  11. package/detectors/bun_ts_metrics_detector/checks/ts_dead_code.mjs +26 -3
  12. package/detectors/bun_ts_metrics_detector/fixtures/clean/domain/offset.ts +12 -0
  13. package/detectors/bun_ts_metrics_detector/fixtures/clean/domain/scale.ts +13 -0
  14. package/detectors/bun_ts_metrics_detector/fixtures/clean/domain/units.ts +7 -0
  15. package/detectors/bun_ts_metrics_detector/fixtures/clean/index.ts +3 -1
  16. package/detectors/bun_ts_metrics_detector/fixtures/clean/package.json +1 -0
  17. package/detectors/bun_ts_metrics_detector/fixtures/clean/tools/banner.ts +2 -0
  18. package/detectors/bun_ts_metrics_detector/fixtures/clean/tools/gen.ts +5 -0
  19. package/detectors/bun_ts_metrics_detector/ts_metrics.mjs +12 -0
  20. package/integrity.json +24 -18
  21. package/lib/scan.mjs +30 -0
  22. package/package.json +1 -1
  23. package/planner-schemas/delivery-config.schema.json +2 -2
  24. package/src/board.ts +14 -3
  25. package/src/delivery.ts +8 -17
@@ -13,7 +13,8 @@ statement: >-
13
13
  terms:
14
14
  - term_id: structural_root
15
15
  text: >-
16
- a file the graph is walked from: index, wagon, composition, main or app.
16
+ a file the graph is walked from: index, wagon, composition, main or app, or a file a script in the nearest
17
+ package.json runs (`bun run docs/gen.ts`), since the repository itself declares it as an entry point.
17
18
  - term_id: type_only_import
18
19
  text: >-
19
20
  an `import type` edge. Erased at compile time, so it creates no runtime edge and cannot keep
@@ -31,6 +31,7 @@ content:
31
31
  exceptions:
32
32
  - Test, spec, and `.d.ts` files are excluded, as are `index.ts` barrels and `__tests__` directories.
33
33
  - Trivial lines (bare braces, closing punctuation) are not counted toward the 7.
34
+ - Import and re-export declarations are not counted, because two files importing the same names from one shared module match because the logic was shared, which is the fix this rule asks for.
34
35
  - Files whose layer cannot be determined are compared only against each other.
35
36
  metadata:
36
37
  aliases:
@@ -31,7 +31,7 @@ content:
31
31
  exceptions:
32
32
  - Occurrences inside comments or string literals are masked and never flagged.
33
33
  - Test files and vendored/build directories are excluded.
34
- - A CLI whose stdout IS its user interface should scope itself out via the consumer scope map.
34
+ - A CLI whose stdout IS its user interface writes that output with `process.stdout.write` (or `process.stderr.write` for its errors), which says plainly that it is the interface, not a log; only `console.*` is matched. atdd-bun has no per-rule scope to exempt a file.
35
35
  metadata:
36
36
  aliases:
37
37
  # LOGGING-PRINT-001 is the SHARED core alias, also carried by the agnostic node and its
@@ -50,7 +50,7 @@ content:
50
50
  hold the stack decision, so it is not suppressible.
51
51
  exceptions:
52
52
  - A `createServer(` call that is not an HTTP server (a test double, an unrelated factory) is a known
53
- imprecision of the name-based match; rename it or scope it out via the consumer scope map.
53
+ imprecision of the name-based match; rename it (atdd-bun has no per-rule scope).
54
54
  - Test files are excluded — spinning a throwaway server in a test is not an application server.
55
55
  - Names appearing inside strings or comments are masked and never flagged.
56
56
  metadata:
@@ -43,8 +43,8 @@ content:
43
43
  exceptions:
44
44
  - A vendored or fixture lockfile inside an excluded tree (`node_modules`, `dist`, `build`,
45
45
  `_generated`) is never scanned.
46
- - A monorepo package that is deliberately NOT on Bun should be scoped out via the consumer scope map,
47
- not suppressed here.
46
+ - atdd-bun has no per-rule scope, so a monorepo package that is deliberately NOT on Bun stays reported until it
47
+ moves to Bun or out of the scanned tree; the finding is the stack decision's open item, not something to suppress.
48
48
  metadata:
49
49
  aliases:
50
50
  - BUN-SINGLE-LOCKFILE-001
@@ -27,12 +27,12 @@ terms:
27
27
  atdd-bun chat post writes the header: from, to, participants and conversation always; kind (task, result, review-request, verdict, rebuttal, status, blocked, heartbeat), stage, reply-to, repo, branch, worktree, goal, base and sha when they apply. The body carries the whole instruction or answer, however long: the thread is the history. A heartbeat carries nothing else; anything that needs an answer goes as blocked, never inside a heartbeat.
28
28
  - term_id: commands
29
29
  text: >-
30
- Post with the body on standard input; read what is on a topic; wait for the next message addressed to you on one or several topics (comma-separated), skipping kinds you do not act on (--skip heartbeat), bounded by --timeout so a tool call ends (exit 2: nothing yet). wait remembers where each identity stopped on each set of topics (~/.atdd-board/cursors), so re-arming a watcher is always the same command; --since still overrides it; follow is the listener for a host that wakes its agent on each line a running process prints (Claude Code's Monitor tool): it streams every message addressed to the identity as it arrives and keeps running, keeping the same saved place; a host that wakes its agent only when a process exits runs wait and re-arms it after each wake-up. One listener per identity. And, for a human, atdd-bun chat alone opens the board: every topic on the left, the selected conversation on the right, live.
30
+ Post with the body on standard input; read what is on a topic; wait for the next message addressed to you on one or several topics (comma-separated), skipping kinds you do not act on (--skip heartbeat), bounded by --timeout so a tool call ends (exit 2: nothing yet). wait remembers where each identity stopped on each set of topics (~/.atdd-board/cursors), so re-arming a watcher is always the same command; --since still overrides it; follow is the listener for a host that wakes its agent on each line a running process prints (Claude Code's Monitor tool): it streams every message addressed to the identity as it arrives and keeps running, keeping the same saved place; a host that wakes its agent only when a process exits runs wait and re-arms it after each wake-up. One listener per identity. When nothing arrives (wait's timeout, or follow's --check-in seconds, 20 minutes by default and 0 to turn it off), the listener prints the owner's check-in, asking where the agent is, whether it is moving or blocked and who knows, whether its stakeholder is informed, and whether it is avoiding ceremony and rabbit holes. The agent acts on it, moving the work or posting once to whoever it waits on, and never posts just to say it is still there. And, for a human, atdd-bun chat alone opens the board: every topic on the left, the selected conversation on the right, live.
31
31
  values:
32
32
  post: atdd-bun chat post <topic> --to <agent> --kind <kind> [--reply-to <id>] < message.md
33
33
  read: atdd-bun chat read <topic> [--mine]
34
34
  wait: atdd-bun chat wait <topic>[,<topic>...] [--since <id>] [--skip heartbeat] --timeout 540
35
- follow: atdd-bun chat follow <topic>[,<topic>...] [--skip heartbeat,progress] [--timeout <seconds>]
35
+ follow: atdd-bun chat follow <topic>[,<topic>...] [--skip heartbeat,progress] [--timeout <seconds>] [--check-in <seconds>]
36
36
  board: atdd-bun chat
37
37
  - term_id: server
38
38
  text: >-
@@ -13,7 +13,7 @@ content:
13
13
  summary: >-
14
14
  The skill and the validators read the same policy. A misspelled key would be ignored by one and not the other, so a malformed block is a finding, and the rest of the run judges evidence against the package defaults.
15
15
  normative_text: |
16
- The policy decides which models may write and review each stage, in preference order, how independent a reviewer must be, and when a model counts as unavailable. It is read by the delivery skill when it dispatches work and by this profile when it judges the record, so both must see the same, valid document.
16
+ The policy decides which models may write and review each stage (an allowed set, in no order; "*" allows any), how independent a reviewer must be, and when a model counts as unavailable. It is read by the delivery skill when it dispatches work and by this profile when it judges the record, so both must see the same, valid document.
17
17
  fix_hint: |
18
18
  Correct the key the finding names. The defaults are:
19
19
 
@@ -4,31 +4,19 @@ kind: rule
4
4
  status: active
5
5
  name: Authors and reviewers are allowed models, and every fallback says why
6
6
  statement: >-
7
- Each work entry's writer appears in that stage's writer list and each review's reviewer in its reviewer list; a model after the first is used only with a recorded fallback from each model before it, stating the kind of unavailability (outage, rate_limit, no_report, timeout), the failures observed (at least delivery.fallback.after_failures) within a window no longer than delivery.fallback.within_minutes, and the reason; or, where the operator ordered the substitution, kind operator with who decided, when, the ruling it is recorded in, and the reason (REQUIRED).
7
+ Each work entry's writer is a model its stage allows for writers, and each review's reviewer a model it allows for reviewers. A stage's list is the set of allowed models, in no order; "*" allows any model. Which allowed model does the work is the operator's choice at any moment, so no fallback has to explain it; the record names the model that actually ran (REQUIRED).
8
8
  terms:
9
9
  - term_id: fallback
10
10
  text: >-
11
11
  a recorded switch from a preferred model to a later one in the stage's list, with role (writer or reviewer) and a reason: an outage, a rate limit, or no auditable report, counted by failures; or the operator's order, referenced by its ruling. A REQUEST CHANGES verdict is never a reason.
12
12
  content:
13
13
  summary: >-
14
- The lists are the operator's choice of who may do the work. A silent switch to another model hides an outage and can hide a reviewer chosen for being lenient.
14
+ The lists are the operator's choice of which models may do the work. Switching between allowed models needs no policy edit and no paperwork, so a rate limit or a preference change is handled on the spot; a model outside the list is still reported.
15
15
  normative_text: |
16
- The first model in a list is used normally. When it is unavailable, the driver falls back to the next one and records it. A fallback is never triggered by a verdict: shopping for a reviewer until one approves is exactly what this rule exists to expose.
17
- An operator can order a substitution faster than a policy change lands, as a standing ruling while a model is rate-limited. The record then states that ruling rather than failures nobody counted. The substitute must still be a model the stage lists in the policy the record is judged by, so the policy file keeps the final word; and only the operator rules: a driver never writes an operator fallback on its own authority.
16
+ The record must name the model that ran. Reviewer independence (a reviewer is never the model that wrote the work it reviews, under different-model) is judged by delivery.reviewer-independent, and a disputed finding upheld on a fresh review blocks the tranche, so switching reviewers cannot buy an approval.
17
+ A fallback entry (kind outage, rate_limit, no_report, timeout or operator) may still record why a model was used; it is optional and, when present, must be well formed.
18
18
  fix_hint: |
19
- Record the fallback on the review:
20
-
21
- fallback:
22
- - { role: reviewer, from: codex, kind: rate_limit, failures: 3, window: { from: "2026-09-25T09:00:00Z", to: "2026-09-25T09:08:00Z" }, reason: "429 on 3 attempts in 10 minutes" }
23
-
24
- or, for a substitution the operator ordered:
25
-
26
- - { role: writer, from: claude-opus, kind: operator, decided_by: "the owner", decided_at: "2026-09-27T23:00:00Z", ruling: "atdd-frg-workstation #abc123", reason: "Standing substitution while Claude is rate-limited: kimi for Opus-tier work" }
27
-
28
- or use a model the stage lists.
29
- exceptions:
30
- - >-
31
- When a stage lists one model, no fallback is possible: an unavailable model blocks the tranche.
19
+ Use a model the stage lists, or have the operator add it (or "*") to the stage's list in atdd-bun.yaml; the integrity check reports an added model once for the operator to approve.
32
20
  metadata:
33
21
  aliases:
34
22
  - DELIVERY-MODEL-ALLOWED-001
@@ -11,13 +11,13 @@ terms:
11
11
  Owns throughput, never the work. It splits the program into tranches with explicit dependencies and writes why, the scope and the split in docs/delivery/index.adoc; activates a tranche once its own dependencies have merged (one still waiting may run plan and its review, nothing after); starts one driver per tranche in its own worktree; keeps provider health for the program, telling every driver to skip a model that is down; intervenes when a tranche is idle without being blocked, or has deliverables and no commit after two hours; and hands BLOCKED disputed-finding to a human with both sides. It never writes, reviews or merges.
12
12
  - term_id: driver
13
13
  text: >-
14
- Owns one tranche end to end. For each stage of delivery.stages, in order, it starts the stage's writer (the first model in its writer list, or the next after a recorded fallback) in the tranche worktree, commits red on its own before green starts, passes the stage's gate, and appends a work entry; for each stage with a reviewer it starts a review. It never reviews.
14
+ Owns one tranche end to end. For each stage of delivery.stages, in order, it starts the stage's writer (any model in its writer list, as the operator chooses) in the tranche worktree, commits red on its own before green starts, passes the stage's gate, and appends a work entry; for each stage with a reviewer it starts a review. It never reviews.
15
15
  - term_id: review_run
16
16
  text: >-
17
17
  Every review is a fresh process in its own detached worktree at the exact SHA (git worktree add --detach), launched with a prompt that gives it the delivery.review convention, the stage, the base SHA, the SHA and the RED commit. Afterwards git status --porcelain must be empty and HEAD still the SHA, or the review is a REVIEWER_FAILURE and does not count. The raw output is kept in the tranche folder and named in the review's report.
18
18
  - term_id: fallback
19
19
  text: >-
20
- After fallback.after_failures failures within fallback.within_minutes (outage, rate limit, no auditable report, timeout), the driver uses the next model in the list and records the fallback with its role, kind, failures and window. A request-changes verdict is never a failure. With the list exhausted, when_exhausted block emits BLOCKED provider-unavailable; wait keeps retrying the last model.
20
+ Any model a stage lists may be used, so a switch needs no fallback: when a model is down or rate-limited, the driver moves to another listed model and records the one that ran. A fallback entry may still say why (outage, rate limit, no auditable report, timeout, or the operator's order); it is optional. A request-changes verdict is never a reason to switch reviewers: a disputed finding upheld on a fresh review blocks the tranche. When no listed model is available, when_exhausted block emits BLOCKED provider-unavailable.
21
21
  - term_id: repair
22
22
  text: >-
23
23
  For each finding of a request-changes review the writer either fixes it or writes one rebuttal with evidence; a fresh review of the same stage follows. If that reviewer upholds a disputed finding, the driver emits BLOCKED disputed-finding and never disputes it again. Any commit, regenerated file, conflict fix or rebase after an approval cancels it and the change goes back to that stage's review.
@@ -42,8 +42,8 @@ content:
42
42
  changed. `strict` — a test the gate cannot actually run is worse than no test.
43
43
  exceptions:
44
44
  - Non-test files are never scanned; a harness import outside a test file is a different smell.
45
- - A test deliberately retained on a foreign runner during a migration should be scoped out via the
46
- consumer scope map, so the exemption is explicit and reviewable rather than invisible.
45
+ - atdd-bun has no per-rule scope, so a test deliberately retained on a foreign runner during a migration stays
46
+ reported until it moves, since the finding is the migration's open item, explicit and reviewable rather than invisible.
47
47
  metadata:
48
48
  aliases:
49
49
  - BUN-TEST-HARNESS-001
@@ -2,7 +2,8 @@
2
2
  // Member check: coder.bun.dead-code-reachability (TypeScript metrics family)
3
3
  //
4
4
  // Cross-file, unlike its siblings: it builds the module import graph, walks it
5
- // from the structural roots (index/wagon/composition/main/app), and reports files
5
+ // from the structural roots (index/wagon/composition/main/app, and files package.json
6
+ // scripts run), and reports files
6
7
  // reached by nothing in either direction.
7
8
  //
8
9
  // The import edges come from Bun's IN-PROCESS TypeScript parser rather than the
@@ -11,10 +12,32 @@
11
12
  // obligation, measured correctly, and only possible because the runtime is Bun.
12
13
  import { walk, readRoots, readExcludes, readText, emit } from "../../../lib/scan.mjs";
13
14
  import * as M from "../ts_metrics.mjs";
14
- import { basename } from "node:path";
15
+ import { basename, dirname, join, resolve } from "node:path";
16
+ import { existsSync } from "node:fs";
15
17
 
16
18
  const RULE = "coder.bun.dead-code-reachability";
17
19
 
20
+ // A file a package.json script runs (`bun run docs/gen.ts`, `bun tools/x.ts`) is an entry point the
21
+ // repository itself declares, so it is a root too. The nearest package.json at or above the scan root.
22
+ function scriptRoots(root, all) {
23
+ for (let dir = resolve(root); ; dir = dirname(dir)) {
24
+ const manifest = join(dir, "package.json");
25
+ if (existsSync(manifest)) {
26
+ let scripts = {};
27
+ try { scripts = JSON.parse(readText(manifest) || "{}").scripts ?? {}; } catch { return []; }
28
+ const out = [];
29
+ for (const command of Object.values(scripts)) {
30
+ for (const [, path] of String(command).matchAll(/(?:^|[\s"'=])((?:\.\/)?[\w@./-]+\.tsx?)(?=$|[\s"';&|)])/g)) {
31
+ const file = join(dir, path);
32
+ if (all.has(file)) out.push(file);
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+ if (dirname(dir) === dir) return [];
38
+ }
39
+ }
40
+
18
41
  const violations = [];
19
42
  const excludes = readExcludes();
20
43
  for (const root of readRoots()) {
@@ -34,7 +57,7 @@ for (const root of readRoots()) {
34
57
  graph.set(f, targets);
35
58
  }
36
59
 
37
- const roots = new Set(files.filter(M.isRootFile));
60
+ const roots = new Set([...files.filter(M.isRootFile), ...scriptRoots(root, all)]);
38
61
  // No structural root means no anchor to measure reachability FROM; reporting
39
62
  // every file as dead would be noise, not a finding. The Python sibling bails
40
63
  // the same way.
@@ -0,0 +1,12 @@
1
+ // The twin of scale.ts's import list, with a different body.
2
+ import {
3
+ one,
4
+ two,
5
+ three,
6
+ four,
7
+ five,
8
+ six,
9
+ } from "./units";
10
+
11
+ // Shift a number by every unit.
12
+ export const offset = (n: number): number => n + one + two + three + four + five + six;
@@ -0,0 +1,13 @@
1
+ // Two modules importing the same names from one shared module is sharing, not duplication:
2
+ // the identical specifier lists below must not be reported.
3
+ import {
4
+ one,
5
+ two,
6
+ three,
7
+ four,
8
+ five,
9
+ six,
10
+ } from "./units";
11
+
12
+ // Scale a number by every unit.
13
+ export const scale = (n: number): number => n * one * two * three * four * five * six;
@@ -0,0 +1,7 @@
1
+ // Small shared helpers that two modules below import by the same long list.
2
+ export const one = 1;
3
+ export const two = 2;
4
+ export const three = 3;
5
+ export const four = 4;
6
+ export const five = 5;
7
+ export const six = 6;
@@ -2,6 +2,8 @@
2
2
  // Imports every module so nothing is unreachable.
3
3
  import { total } from "./domain/total";
4
4
  import { label } from "./domain/label";
5
+ import { scale } from "./domain/scale";
6
+ import { offset } from "./domain/offset";
5
7
 
6
8
  // Run the tiny pipeline this fixture represents.
7
- export const run = (xs: number[]): string => label(total(xs));
9
+ export const run = (xs: number[]): string => label(offset(scale(total(xs))));
@@ -0,0 +1 @@
1
+ { "private": true, "scripts": { "gen": "bun run tools/gen.ts --check" } }
@@ -0,0 +1,2 @@
1
+ // Reached only through tools/gen.ts, which a package.json script runs.
2
+ export const banner = (name: string): string => `== ${name} ==`;
@@ -0,0 +1,5 @@
1
+ // A tool run by a package.json script: the script is its entry point, so it is live.
2
+ import { banner } from "./banner";
3
+
4
+ // Print the banner the script exists to print.
5
+ console.log(banner("fixture"));
@@ -228,12 +228,24 @@ export function normalizeLine(line) {
228
228
  const TRIVIAL = new Set(["", "{", "}", "};", ");", "],", ")", "]", "});"]);
229
229
  export const isTrivial = (n) => TRIVIAL.has(n);
230
230
 
231
+ // Import and re-export declarations are not code: two files that import the same names
232
+ // from one shared module match line for line BECAUSE the logic was shared, so the lines of
233
+ // a declaration (one line, or `import {` through `} from "…"`) are left out of the windows.
234
+ const DECLARATION = /^(import\b|export (type )?(\{|\*))/;
235
+ const DECLARATION_END = /\bfrom "S"|^import "S"|\}/;
236
+
231
237
  // A 16-hex-char digest of each `minLines` window of non-trivial normalized lines —
232
238
  // the same fingerprint shape the Python detector hashes with sha256[:16].
233
239
  export function fragments(source, minLines, hasher) {
234
240
  const nonTrivial = [];
241
+ let inDeclaration = false;
235
242
  source.split("\n").forEach((line, i) => {
236
243
  const n = normalizeLine(line);
244
+ if (!inDeclaration && DECLARATION.test(n)) inDeclaration = true;
245
+ if (inDeclaration) {
246
+ if (DECLARATION_END.test(n)) inDeclaration = false;
247
+ return;
248
+ }
237
249
  if (!isTrivial(n)) nonTrivial.push([i + 1, n]);
238
250
  });
239
251
  if (nonTrivial.length < minLines) return [];
package/integrity.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.27",
2
+ "version": "0.10.29",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
@@ -30,7 +30,7 @@
30
30
  "conventions/coder.bun/coder.bun.complexity-nesting.convention.yaml": "7d5a37ee0d5ccb9e97c0ba042f5c2e858e06f2361b6a47e687ec5af372c887b8",
31
31
  "conventions/coder.bun/coder.bun.composition-consumer.convention.yaml": "5bd69cdf8cb6a73e692c73cad30dceb533f624b38a6a120fc4f8e660a2cf25e0",
32
32
  "conventions/coder.bun/coder.bun.composition-root.convention.yaml": "74e30a8fa55b304ba2922fec59ecd8626d7a3c61411bb46bc5249188607f0de5",
33
- "conventions/coder.bun/coder.bun.dead-code-reachability.convention.yaml": "1e5e1c635e9fe87990c15c924d3079a019c3f848b09d05bcc0ab40a9a6bcdc1f",
33
+ "conventions/coder.bun/coder.bun.dead-code-reachability.convention.yaml": "bce2c0f69c5c4f31a43278f3eabe993591e823cdb714f0841361ab470a134798",
34
34
  "conventions/coder.bun/coder.bun.design-connected.convention.yaml": "21d9b1f11cb92f161acd163fe94d79022a0b35e269ea0db1c9bccbcfa4241b69",
35
35
  "conventions/coder.bun/coder.bun.design-dependency-flow.convention.yaml": "e96b6850ce8d5811947a9d4b62855477a112c053524a1d97a4f7ba35efb17e07",
36
36
  "conventions/coder.bun/coder.bun.design-hierarchy-import.convention.yaml": "067de2a858dba47ec11fab3ebbae052ea15d1481d9cb702a5eacc996c74c5922",
@@ -43,7 +43,7 @@
43
43
  "conventions/coder.bun/coder.bun.dto-mapper.convention.yaml": "e6890bcb421e9c10e71788e92ba1a2f1f96eed222fd0f3259f81b175b28ec190",
44
44
  "conventions/coder.bun/coder.bun.dto-placement.convention.yaml": "488576b8eba889b008e928357e62c323784da640bdcb986a06152ac4e4424568",
45
45
  "conventions/coder.bun/coder.bun.dto-purity.convention.yaml": "2c87391f323b10e8545095c8b278547a5a4f3854ac02ddec1849f4a6c83676fc",
46
- "conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml": "76ce7ca55bbab2ce2b199e73436330cdf42a0d7ac410afaa13097031a02b2206",
46
+ "conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml": "5899b2b612108509a89be54db2c6f65f7e8fd798d6828ce81dd7d67e8ae4ae0c",
47
47
  "conventions/coder.bun/coder.bun.error-response-bare-string.convention.yaml": "c2c0f859f6dd12702edf225c4148849e72b6c25e6d7cd28edd5d644ba277a23c",
48
48
  "conventions/coder.bun/coder.bun.error-response-code-format.convention.yaml": "3048fed00b92c2559f7c52f73cfcce07b583eb47b12d04f0851e237ae3646567",
49
49
  "conventions/coder.bun/coder.bun.green-header-order.convention.yaml": "8b00d89605cbd618c052805fb1129a7a65743c72a7627f7cd08f0685ddefa8aa",
@@ -63,7 +63,7 @@
63
63
  "conventions/coder.bun/coder.bun.interlocking-runner-exists.convention.yaml": "f7fadc580d2896f7d8341f555e4447480809329f3901209d30c2e54181032f15",
64
64
  "conventions/coder.bun/coder.bun.journey-runner-boundary.convention.yaml": "51df6caa810d887a84fc8196bc103465685400f12ba8a0504efef9b88c10fe33",
65
65
  "conventions/coder.bun/coder.bun.layer-naming.convention.yaml": "009c21f6adbbc1e08d9781d7466b06e3d7fb7d935bf45b1df07fe37639a60a82",
66
- "conventions/coder.bun/coder.bun.logging-console.convention.yaml": "b0e5765b8efba245cdd133f8321ba2364acbfae26a302f09e25cc905d652ae28",
66
+ "conventions/coder.bun/coder.bun.logging-console.convention.yaml": "e85fc1038ac78799b36d1f4156b6547fb00441f1e3ac3ec93e59e7e895e37aef",
67
67
  "conventions/coder.bun/coder.bun.logging-structured.convention.yaml": "a7c8bd90762bf1516d2af63d4fe731720bec9f58431009f69af1bfebc0398759",
68
68
  "conventions/coder.bun/coder.bun.quality-comments.convention.yaml": "fcd42efd152c4815cd3fd7811b091321e4a0d5e13f9c251cfd82b248f27c9fd8",
69
69
  "conventions/coder.bun/coder.bun.quality-mi.convention.yaml": "13dd95a87d67cb9b765b41cebcc3d33b117725edc28798a7d258cd6a66363621",
@@ -71,8 +71,8 @@
71
71
  "conventions/coder.bun/coder.bun.responsive-no-fixed-width.convention.yaml": "09bb70ccb5350bd46007ba1cf7da55447bdb87b8b31677dacba2bddba5ea63ab",
72
72
  "conventions/coder.bun/coder.bun.responsive-viewport-meta.convention.yaml": "705b1ed788a9c5b6d6a555d96662269e09c4ba5b30a7000d37ddf91393a65a88",
73
73
  "conventions/coder.bun/coder.bun.runtime-executes-the-declaration.convention.yaml": "ed1643e92878a746298a20bfbc301c1f469d123d71ca272de1384d2f7cc03ba1",
74
- "conventions/coder.bun/coder.bun.runtime-server-is-bun-serve.convention.yaml": "ded3ff738a38e7f72e28df98ddc94bab9674732de7b5a62c5695f356b01ac9f0",
75
- "conventions/coder.bun/coder.bun.runtime-single-lockfile.convention.yaml": "eb3ff05c06fb37e05c8931742e589d6fe060728642140a79db3f9fc556fa6f94",
74
+ "conventions/coder.bun/coder.bun.runtime-server-is-bun-serve.convention.yaml": "43f3a709e59bf564eee5691ee57a92b4c92d9bea6cedbcdf060b692c14d47663",
75
+ "conventions/coder.bun/coder.bun.runtime-single-lockfile.convention.yaml": "93a90a41e39fa08461f86dc5585a30459120db69957b35f3840b44c949395dec",
76
76
  "conventions/coder.bun/coder.bun.runtime-typecheck-enforced.convention.yaml": "375bce699e7ed25c8d4024bc3af3ad7ce007bb6260b096c212a063e2a7841744",
77
77
  "conventions/coder.bun/coder.bun.security-hardcoded-secret.convention.yaml": "1a57d91aa91c52cc1efdf4168a2f3f147e7f25e979c47161d9757e1839ebbddd",
78
78
  "conventions/coder.bun/coder.bun.security-missing-auth.convention.yaml": "2b937475e7a1f3f2718be6a3f0d94fa261ca65e72bbf8e3f2ca39ba143b90544",
@@ -93,13 +93,13 @@
93
93
  "conventions/coder.htmx/coder.htmx.verb-endpoint-same-origin.convention.yaml": "2128d26153e2d1f19c65a1e1665d4f82c78395daedb336d6ef1256e82b74aedb",
94
94
  "conventions/coder.htmx/coder.htmx.verb-mutation-signals-progress.convention.yaml": "ae85c5ea01a09a3c41b939988088a8e5da91848d2710a1f0a1e0c86308638ac9",
95
95
  "conventions/delivery/delivery.approved-sha-resolves.convention.yaml": "68f2ecc10870e780abb05bb31d211b1923d47b56672df4b3950f437aa9cf8ca8",
96
- "conventions/delivery/delivery.board.convention.yaml": "4c544f51de80b09961a8df41028867023ab802aa4b31dcb0a1ce29e890552e6d",
97
- "conventions/delivery/delivery.config-schema.convention.yaml": "4c7b449074a8bd9f89b70c702ab3e0df229105ddb4458772f894f0bf37813e9f",
96
+ "conventions/delivery/delivery.board.convention.yaml": "fefc9f32f8ee85ee35b254cbc4aefec55a4b72eaa3a7b02b0ec21efb2074a013",
97
+ "conventions/delivery/delivery.config-schema.convention.yaml": "b59a69b7789ad0c3d0b1ac9ea2b955fbbb5dfa07a9ee169a037b406a198813f0",
98
98
  "conventions/delivery/delivery.evidence-schema.convention.yaml": "9c6a22495dfb3e2fcbca700392866c3b5660084b1f5594230eb320b499d5e5cb",
99
99
  "conventions/delivery/delivery.findings-resolved.convention.yaml": "f6574e93a9a2d4bdd6d6f5645720b1a647fc0048d95f7675fe783d2ddb62c05b",
100
100
  "conventions/delivery/delivery.merge-gate.convention.yaml": "48745ec23625b68be9b8fe2c6dcdcba88b2b38924f6593abf6987391ff0ee243",
101
- "conventions/delivery/delivery.model-allowed.convention.yaml": "7bf69b1803ec388a77b2fbc8aa727ff4ac6fd0cf4e5d5736519117bf86ac8688",
102
- "conventions/delivery/delivery.operating-model.convention.yaml": "7982157d957938ade878a114d3f5c2bdaa8f02e52ac21614634d9af4be9692f1",
101
+ "conventions/delivery/delivery.model-allowed.convention.yaml": "e9690e95f3604483f182b3032ae60eac20109eb458509f14651945ccf07692f2",
102
+ "conventions/delivery/delivery.operating-model.convention.yaml": "1a86a1fe84695968a2fb719a899629af9a8baadcd90bdb8776f1f0208cebbbb4",
103
103
  "conventions/delivery/delivery.review.convention.yaml": "b033579c2d90fcb098d325dcc07c7b084e048cca43fbe8e28159ec08a412018a",
104
104
  "conventions/delivery/delivery.reviewer-independent.convention.yaml": "8c436d66ef5b1f6d6bc5dd55a235caca2490020e88665eecadb2ca41cd4149c5",
105
105
  "conventions/delivery/delivery.stages-complete.convention.yaml": "0e5dd8057ad51eff7906e3424a9686a81be1814447afb9a30acab43d39e49b4c",
@@ -139,7 +139,7 @@
139
139
  "conventions/tester.bun/tester.bun.telemetry-test-binding.convention.yaml": "4566199fd401cd8666e08466cd3a5236f23ef356364e673b36a2e3c5ec78638d",
140
140
  "conventions/tester.bun/tester.bun.telemetry-timing-semantics.convention.yaml": "5494744872deb34c175d741dfc79e9cfa720808dd7efaf118f709af417a1dd39",
141
141
  "conventions/tester.bun/tester.bun.test-carries-urn-identity.convention.yaml": "4c5846bae7f9671eabf808f43ee3f134a67214113f0e15e9a60705dbdcbe8d10",
142
- "conventions/tester.bun/tester.bun.test-imports-bun-test.convention.yaml": "b447312046391cdac30f76d35624b23dc1a72cb87225d0f2a4610fb3188425dc",
142
+ "conventions/tester.bun/tester.bun.test-imports-bun-test.convention.yaml": "6eef1c745315a69357d23bf07d318e504d425f76fcfdc120166b8ef2d4c820d6",
143
143
  "conventions/tester.bun/tester.bun.test-isolation-no-live-state.convention.yaml": "3488af16431ea461e9128d15dbb0ad86324f4446bf36d5a662fab73687e8860e",
144
144
  "conventions/tester.bun/tester.bun.test-no-self-skip.convention.yaml": "23fca575611f9c0740bcf64849a3c1600ef4cc1cc64df3c106d328b67283d8a3",
145
145
  "conventions/tester.bun/tester.bun.test-phase-declared.convention.yaml": "cd22101bfcbc2eeca3da1aa2907ba6c944674c31cfaac5305f2149c6c429cf87",
@@ -649,14 +649,20 @@
649
649
  "detectors/bun_ts_metrics_detector/checks/ts_complexity_cyclomatic.mjs": "56640b3a0f565c45a73b7c174adb1d6b4b6ac24d286a907bc1426f013cd6039e",
650
650
  "detectors/bun_ts_metrics_detector/checks/ts_complexity_length.mjs": "e49fe93dbf2c52a91fe97b1e0b810d60f7d34d43581dfb96d4827640e6f5e3fd",
651
651
  "detectors/bun_ts_metrics_detector/checks/ts_complexity_nesting.mjs": "8d8f61ea1748f33b037beb3f3fe5c84acbce395575e24fb1d01432813762e4d1",
652
- "detectors/bun_ts_metrics_detector/checks/ts_dead_code.mjs": "05ec3cee78ef0413d8d7f71705a19aba80f84eab96c100868a0791eafb38dded",
652
+ "detectors/bun_ts_metrics_detector/checks/ts_dead_code.mjs": "154fdf7b8e896f73ae86d7a9609a953fcb8d33c1b61eabcb6d190b06d64d97b4",
653
653
  "detectors/bun_ts_metrics_detector/checks/ts_duplication.mjs": "12b5087c34741ce3fde573059d1afbaafc80ce1bfe8a235780113fd558dbbc7d",
654
654
  "detectors/bun_ts_metrics_detector/checks/ts_quality_comments.mjs": "872d71d2165eba4cb39c975b09365cf67a64f1db5c55c0df9e86ff411e02ea31",
655
655
  "detectors/bun_ts_metrics_detector/checks/ts_quality_mi.mjs": "69765d88441e98f4f38b7da0a416b7dab8b7f0072e562ea9d40d399802452067",
656
656
  "detectors/bun_ts_metrics_detector/detect.mjs": "c98af0088388328db9fa014fc804fb4239627a85598d3654acdf66209cea619a",
657
657
  "detectors/bun_ts_metrics_detector/fixtures/clean/domain/label.ts": "51e6fce80de70c168c750a2a71d93cde98a4a7fe2f29b4ff593ea2a531e1611a",
658
+ "detectors/bun_ts_metrics_detector/fixtures/clean/domain/offset.ts": "04b765ce74910a128e02c2a08676fd5b673cb952f2b8e2ee32fa5502a8ecb4a8",
659
+ "detectors/bun_ts_metrics_detector/fixtures/clean/domain/scale.ts": "fba087fb7b9a87a450f42a7d22119c8180b2093d25f819029f3004c815ce0863",
658
660
  "detectors/bun_ts_metrics_detector/fixtures/clean/domain/total.ts": "af50d27155a6cb98acffad732eecdfe1c0d56c26efa0f2fcfc76e5f7f16b429d",
659
- "detectors/bun_ts_metrics_detector/fixtures/clean/index.ts": "d64b7246426919e7a14a93df0648c65fb6bc2d17f3b2fc39b99090d0e73499c7",
661
+ "detectors/bun_ts_metrics_detector/fixtures/clean/domain/units.ts": "13d1013fe94ae57d2f455e882c93472caec4be862636763965f12cdbffd5ee5f",
662
+ "detectors/bun_ts_metrics_detector/fixtures/clean/index.ts": "8b023204f14afe36bd393bc6d3486ce1d737c7c1d6037153d7e53083de041da6",
663
+ "detectors/bun_ts_metrics_detector/fixtures/clean/package.json": "abd8243a7b615fc3765a9d1657965c515d2f5a971b7c76157d171d36fc5e84cf",
664
+ "detectors/bun_ts_metrics_detector/fixtures/clean/tools/banner.ts": "60e99be98df81bd287f4be011559a4baa3badb73a5667511329e4ec406bed3ca",
665
+ "detectors/bun_ts_metrics_detector/fixtures/clean/tools/gen.ts": "7540d0baa666cc05ad65326fc7bcabc464d9efae13a228e3365b999be5863a1a",
660
666
  "detectors/bun_ts_metrics_detector/fixtures/dirty/bare.ts": "9f221d09cb5ee237d82329ef7ea878a00cb214707d8907bbc45894c14a7804c0",
661
667
  "detectors/bun_ts_metrics_detector/fixtures/dirty/big.ts": "3989b9170c02035b0394d6a12324eb66a78c68148431d18ae616392b59072cb6",
662
668
  "detectors/bun_ts_metrics_detector/fixtures/dirty/consumer.ts": "f05d34e097667797806ecfdd34fab5def16983eb285c654924d92ae7ff3ccfb6",
@@ -667,7 +673,7 @@
667
673
  "detectors/bun_ts_metrics_detector/fixtures/dirty/long.ts": "b87149b1b550a00bc2de2b2b21a4d9925572c002221941fae8135e8fe0c27cd1",
668
674
  "detectors/bun_ts_metrics_detector/fixtures/dirty/orphan.ts": "0f7acd0dffe90902a0a43cc45771b8a473d079efc9693112b3b85645c191c23d",
669
675
  "detectors/bun_ts_metrics_detector/fixtures/dirty/typed.ts": "7c27ee1a674af7d5b2cce3a65bfec2111602d84a75817fd60f737c7d63fc9c83",
670
- "detectors/bun_ts_metrics_detector/ts_metrics.mjs": "6fe397d634e05a0c9605dcefcec53a5736eab4bcc5c45adec55fb8a3de9b3c6d",
676
+ "detectors/bun_ts_metrics_detector/ts_metrics.mjs": "d5da8c768dd7fe1717cc7f29b4115f8b2573c945cf9fb2e6d7c2d76adc186417",
671
677
  "detectors/delivery_evidence/atdd.implementation.yaml": "2f6fc30dc353bef97bcb32ea54e4317d3c87cbd7ae1443fcef2aefc46280dc09",
672
678
  "detectors/delivery_evidence/detect.mjs": "e98664dee8090a0b280cd285a54e9a6fb9ba3572af2666b4e3656ca5b0555509",
673
679
  "detectors/delivery_evidence/fixtures/clean/atdd-bun.yaml": "f701861e3530bb5a25a70d73e248549fb96642f48db1ff4d3e8e4546aaa825ee",
@@ -971,7 +977,7 @@
971
977
  "lib/frontend.mjs": "d6d9b29c3d7f2d162cda03bb34b20a3224a82887b6be3db7cab375510913c9a3",
972
978
  "lib/imports.mjs": "3db4979269488fa26f6e7aefded6ece9c646be6587a10e249821a5a208109962",
973
979
  "lib/interlocking.mjs": "d0803d080ac7d7477c51be70112df1e85213d339fd3c6b71f75f26145fa8accd",
974
- "lib/scan.mjs": "925847f91a46c8ca1bdcfd44a047e67c68441a047c51ff496b253b422baddb97",
980
+ "lib/scan.mjs": "7a4410f3bc2e2ebbdddb4f3dd6268987a01b9685e94c8a18ea05b4056daf4ea8",
975
981
  "planner-nodes/ENFORCEMENT_SCOPE.yaml": "d8ec7039c219857f3d77af5d4d84612b533b69c1a5fcef935fbf46470adafed4",
976
982
  "planner-nodes/nodes/planner.acceptance.abstract-fields-required.convention.yaml": "bd565693f0ae861f0561ad2f7a91a8c55f9a209c3acd59c5750cfcffd96a85d8",
977
983
  "planner-nodes/nodes/planner.acceptance.authoring-guidelines.convention.yaml": "16348dceed5868041b832bde766e9c8d4a950d4032f1f3066b357644081a3b17",
@@ -1179,7 +1185,7 @@
1179
1185
  "planner-schemas/binding-lock.schema.json": "f2455606702f45fafa194549126d4a66e6c286ba1c4ba9e6bdcf932286a6d34d",
1180
1186
  "planner-schemas/component.schema.json": "d20d3502c155627a120cb10d5e9bdec4ba2c73af9c3e9a9d10e3c351f167c72a",
1181
1187
  "planner-schemas/contract-registry.schema.json": "b9dbaf6c87d4f4d676e3b4251344b9eada523b1c5e739c0423cabe267012c12e",
1182
- "planner-schemas/delivery-config.schema.json": "16a9b66a7d36edf8a49c68d9786f2516cd2ff5392be6dd60b766a09ffc95a9c6",
1188
+ "planner-schemas/delivery-config.schema.json": "7cd9c15ea4345ad83195b9a1c7d09a0b66275e9a3fdf8b5d6905d96bbec579d3",
1183
1189
  "planner-schemas/delivery-evidence.schema.json": "72ba1450abe5482c43f2069e9ee7d121398008a0ddef36c6b269d38a16872e0e",
1184
1190
  "planner-schemas/feature.schema.json": "eb4aa40d89067ae5c86a079bb3d8e4f45c8727f9e15db2b86c08dacf8e4ae56a",
1185
1191
  "planner-schemas/journey.schema.json": "1ff705bcf89a2823cf60c9e1597e254f97844e7a74dc58299d53192ad2c9f8c3",
@@ -1196,10 +1202,10 @@
1196
1202
  "relationships.yaml": "1cb713364aff54fb6e299e0737fe75a5d7b2e1dbd0a79b16b793aa1d269207b5",
1197
1203
  "src/agent.ts": "65e4579adb5a54977f557f8c783aba0e2a2a1d5cf3cea9f017b67ffe598b2f65",
1198
1204
  "src/board-ui.ts": "16a843705552f8f18e0e328e90e4a2a9148d771ca69f2708fe4a91302947dfad",
1199
- "src/board.ts": "bc70e479874f2f05a44293226e53eb9e763b661bdcebade4f8948b0dd2ee2ead",
1205
+ "src/board.ts": "6f46cd890e0dfeb4a928d969e902474fb059844561a111ca8bbfeb4ff3e2d83b",
1200
1206
  "src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
1201
1207
  "src/cli.ts": "e5ab4d2435fdcbebf462b4c2b67b495bd975d5deb668b088d8cba04a9d8b78a5",
1202
- "src/delivery.ts": "f6bb6a7c42693d795b964c77b6186e1ca3a1556fd43932911a0ad42e72e7e441",
1208
+ "src/delivery.ts": "2eaaf4fd87aea26dce70b4c88a4e0a2f25b9de165bc2e38330fd7eb449587901",
1203
1209
  "src/docs-capability.ts": "8a7a4360976773fe462b1860eaf26182b016bcb1fbcd6a6d5423bd81379dbe67",
1204
1210
  "src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
1205
1211
  "src/hooks.ts": "2feeec2674c1462c94256de02ce59ccc6e3ff671ec4dca1bf9903c32888b600b",
package/lib/scan.mjs CHANGED
@@ -115,6 +115,23 @@ export function readExcludes() {
115
115
  // length, preserving every line and column offset. A check that greps raw source
116
116
  // otherwise fires on the rule's own name inside a comment or a doc string — the
117
117
  // classic detector false positive.
118
+ // A `/` starts a regex literal where an expression may begin: at the start, after an operator or opening
119
+ // punctuation, or after a keyword such as `return`. After a name, a number or `)` / `]` it is division.
120
+ const REGEX_AFTER_WORD = new Set(["return", "typeof", "case", "do", "else", "in", "of", "new", "delete", "void", "throw", "yield", "await", "instanceof"]);
121
+ function regexMayStart(text, i) {
122
+ let k = i - 1;
123
+ while (k >= 0 && /\s/.test(text[k])) k--;
124
+ if (k < 0) return true;
125
+ const prev = text[k];
126
+ if ("(,=:[!&|?{};+-*%>~^".includes(prev)) return true; // not `<`: in TSX, `</b>` closes a tag
127
+ if (/[\w$]/.test(prev)) {
128
+ let w = k;
129
+ while (w >= 0 && /[\w$]/.test(text[w])) w--;
130
+ return REGEX_AFTER_WORD.has(text.slice(w + 1, k + 1));
131
+ }
132
+ return false;
133
+ }
134
+
118
135
  export function maskLiteralsAndComments(text) {
119
136
  const out = text.split("");
120
137
  let i = 0;
@@ -135,6 +152,19 @@ export function maskLiteralsAndComments(text) {
135
152
  j = j === -1 ? n : j + 2;
136
153
  blank(i, j);
137
154
  i = j;
155
+ } else if (ch === "/" && regexMayStart(text, i)) {
156
+ // A regex literal, masked like a string: up to the unescaped `/` outside a character class, never past the line.
157
+ let j = i + 1, inClass = false;
158
+ while (j < n && text[j] !== "\n") {
159
+ if (text[j] === "\\") { j += 2; continue; }
160
+ if (text[j] === "[") inClass = true;
161
+ else if (text[j] === "]") inClass = false;
162
+ else if (text[j] === "/" && !inClass) break;
163
+ j++;
164
+ }
165
+ if (j >= n || text[j] !== "/") { i++; continue; } // no closing slash on the line: not a regex
166
+ blank(i + 1, j);
167
+ i = j + 1;
138
168
  } else if (ch === '"' || ch === "'" || ch === "`") {
139
169
  let j = i + 1;
140
170
  while (j < n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.27",
3
+ "version": "0.10.29",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
@@ -76,8 +76,8 @@
76
76
  "type": "array",
77
77
  "minItems": 1,
78
78
  "uniqueItems": true,
79
- "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
80
- "description": "Models in preference order: the first is used normally, each later one only as a recorded fallback."
79
+ "items": { "type": "string", "pattern": "^(\\*|[a-z][a-z0-9-]*)$" },
80
+ "description": "The models allowed for the role, in no order: any listed model may do the work, and \"*\" allows any model."
81
81
  },
82
82
  "stage": {
83
83
  "type": "object",
package/src/board.ts CHANGED
@@ -111,6 +111,13 @@ export async function read(url: string, topics: string[], since = "all", signal?
111
111
  /** The longest one poll may take. ntfy answers a poll at once, so only a stalled request ever reaches it. */
112
112
  const POLL_LIMIT_MS = 10_000;
113
113
 
114
+ /** What a listener prints when nothing has arrived for a while: the owner's check-in, for the agent to act on (move
115
+ * the work, or tell whoever it waits on), never to answer with a "still here" post. */
116
+ export function checkIn(agent: string, quietSeconds: number): string {
117
+ const quiet = quietSeconds <= 0 ? "" : quietSeconds < 60 ? ` for ${quietSeconds} s` : ` for ${Math.round(quietSeconds / 60)} min`;
118
+ return `CHECK-IN for ${agent}: no message${quiet}. Where are you? Are you moving forward, or blocked on someone, and do they know? Is your stakeholder informed? Are you avoiding ceremony and rabbit holes, effectiveness first, then efficiency? Act on it: move the work, or post once to whoever you wait on; never post just to say you are still here. Then re-arm this listener.`;
119
+ }
120
+
114
121
  export async function waitFor(url: string, topics: string[], agent: string, since = "all", timeoutSeconds = 0, skip: string[] = [], intervalMs = 500, seen: (id: string) => void = () => {}): Promise<Message | null> {
115
122
  const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
116
123
  for (;;) {
@@ -170,7 +177,7 @@ export async function chat(args: string[], env: Record<string, string | undefine
170
177
  // Without --since, continue from where this identity last stopped on these topics; either way, remember the place.
171
178
  const file = cursorFile(me, topics, env);
172
179
  const message = await waitFor(url, topics, me, flag("since") || loadCursor(file) || seedCursor(me, env) || "all", Number(flag("timeout") ?? 0), list(flag("skip")), 500, id => saveCursor(file, id));
173
- if (!message) { console.error(`no message for ${me} on ${topics.join(", ")} yet; run the same wait again to continue`); return 2; }
180
+ if (!message) { console.log(checkIn(me, Number(flag("timeout") ?? 0))); return 2; }
174
181
  console.log(format(message, topics.length > 1));
175
182
  return 0;
176
183
  }
@@ -180,11 +187,15 @@ export async function chat(args: string[], env: Record<string, string | undefine
180
187
  const file = cursorFile(me, topics, env), skip = list(flag("skip")), total = Number(flag("timeout") ?? 0);
181
188
  const deadline = total > 0 ? Date.now() + total * 1000 : Infinity;
182
189
  let since = flag("since") || loadCursor(file) || seedCursor(me, env) || "all";
190
+ // After --check-in seconds without a message (default 20 minutes; 0 = never), the agent is asked to check itself.
191
+ const quietLimit = Number(flag("check-in") ?? 1200) * 1000;
192
+ let quietSince = Date.now();
183
193
  for (;;) {
184
194
  const left = Math.ceil((deadline - Date.now()) / 1000);
185
195
  if (left <= 0) return 0;
186
- const message = await waitFor(url, topics, me, since, Math.min(60, left), skip, 500, id => { since = id; saveCursor(file, id); });
187
- if (message) console.log(format(message, topics.length > 1));
196
+ const message = await waitFor(url, topics, me, since, Math.min(60, left, quietLimit > 0 ? Math.max(1, Math.ceil((quietSince + quietLimit - Date.now()) / 1000)) : 60), skip, 500, id => { since = id; saveCursor(file, id); });
197
+ if (message) { console.log(format(message, topics.length > 1)); quietSince = Date.now(); }
198
+ else if (quietLimit > 0 && Date.now() - quietSince >= quietLimit) { console.log(checkIn(me, Math.round((Date.now() - quietSince) / 1000))); quietSince = Date.now(); }
188
199
  }
189
200
  }
190
201
  if (topics.length !== 1) throw new Error(`${command} takes one topic`);
package/src/delivery.ts CHANGED
@@ -39,7 +39,7 @@ export type DeliveryPolicy = {
39
39
  };
40
40
 
41
41
  /** The default operating model: two reviews, the plan and the whole change; the stages between them are written and
42
- * held by their deterministic gates. Lists are preference orders; a later model is used only as a recorded fallback. */
42
+ * held by their deterministic gates. Each list is the set of models allowed for the role, in no order; "*" allows any. */
43
43
  const DEFAULT_STAGES: Partial<Record<Stage, { writer?: string[]; reviewer?: string[] }>> = {
44
44
  plan: { writer: ["codex", "claude-opus", "kimi"], reviewer: ["glm", "claude-opus", "codex", "kimi"] },
45
45
  red: { writer: ["glm", "claude-sonnet", "deepseek-flash", "claude-opus", "codex", "kimi"] },
@@ -122,10 +122,7 @@ export function loosenedDelivery(base: unknown, current: unknown): string[] {
122
122
  for (const role of ["reviewer", "writer"] as const) {
123
123
  const added = c[role].filter(model => !b[role].includes(model));
124
124
  if (added.length && b[role].length) out.push(`delivery.stages.${stage}.${role} adds ${added.join(", ")}`);
125
- // The lists are preference orders: moving a model earlier, by reordering or removing one before it, makes a
126
- // fallback model usable without the fallback.
127
- const promoted = c[role].filter(model => b[role].includes(model) && c[role].indexOf(model) < b[role].indexOf(model));
128
- if (promoted.length) out.push(`delivery.stages.${stage}.${role} [${b[role].join(", ")}] → [${c[role].join(", ")}] promotes ${promoted.join(", ")}`);
125
+ // The lists are allowed sets: reordering one changes nothing; only an added model (or "*") is a loosening.
129
126
  }
130
127
  // A stage that had no writer list now accepting writers is a loosening too: before, nothing could be written there.
131
128
  if (!b.writer.length && c.writer.length) out.push(`delivery.stages.${stage}.writer adds ${c.writer.join(", ")}`);
@@ -235,18 +232,12 @@ export async function loadEvidence(root: string, policy: DeliveryPolicy): Promis
235
232
  return out;
236
233
  }
237
234
 
238
- /** A model must come from the stage's list; a model after the first needs a recorded fallback from each one before it,
239
- * except one the stage could not use: under different-model independence a reviewer skips every model that wrote the
240
- * work it reviews (delivery.reviewer-independent's fix hint). Ineligible is not unavailable, so no fallback records it. */
241
- function checkModel(file: string, at: string, stage: Stage, role: "writer" | "reviewer", actor: Actor, list: string[], fallbacks: Fallback[], defaultRole: "writer" | "reviewer", fallback: DeliveryPolicy["fallback"], ineligible: string[] = []): PlanFinding[] {
242
- const out: PlanFinding[] = [];
243
- const index = list.indexOf(actor.model);
244
- if (index < 0) return [finding("delivery.model-allowed", file, `${at}: ${role} model '${actor.model}' is not in ${stage}.${role} [${list.join(", ")}]`)];
245
- // `author` is the 0.9 spelling of `writer`.
246
- const recorded = fallbacks.filter(entry => ((entry.role === "author" ? "writer" : entry.role) ?? defaultRole) === role).map(entry => entry.from);
247
- for (const skipped of list.slice(0, index)) if (!recorded.includes(skipped) && !ineligible.includes(skipped)) out.push(finding("delivery.model-allowed", file, `${at}: ${role} '${actor.model}' is a fallback, but no fallback from '${skipped}' records why it was unavailable`));
248
- for (const from of recorded) if (!list.slice(0, index).includes(from)) out.push(finding("delivery.model-allowed", file, `${at}: ${role} fallback from '${from}' does not precede '${actor.model}' in [${list.join(", ")}]`));
249
- return out;
235
+ /** A model must be one the stage allows: its list is the set of allowed models, in no order, and "*" allows any. Which
236
+ * listed model does the work is the operator's choice at any moment, so no fallback has to explain it; the record still
237
+ * names the model that ran, and reviewer independence is judged separately (delivery.reviewer-independent). */
238
+ function checkModel(file: string, at: string, stage: Stage, role: "writer" | "reviewer", actor: Actor, list: string[], _fallbacks: Fallback[], _defaultRole: "writer" | "reviewer", _fallback: DeliveryPolicy["fallback"], _ineligible: string[] = []): PlanFinding[] {
239
+ if (list.includes("*") || list.includes(actor.model)) return [];
240
+ return [finding("delivery.model-allowed", file, `${at}: ${role} model '${actor.model}' is not allowed for ${stage}.${role} [${list.join(", ")}]`)];
250
241
  }
251
242
 
252
243
  /** The count and window of every recorded fallback are the driver's claims, but explicit ones: outside the policy is out of policy. */