@caddis/cli 0.4.52 → 0.4.54

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 (111) hide show
  1. package/bundles/antigravity-plugin/CADDIS-GLOSSARY.md +1 -0
  2. package/bundles/antigravity-plugin/guard_agy.py +184 -0
  3. package/bundles/antigravity-plugin/plugin.json +1 -1
  4. package/bundles/antigravity-plugin/repo_freshness.py +132 -5
  5. package/bundles/antigravity-plugin/scripts/caddis_budget.py +283 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +227 -0
  7. package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +284 -0
  8. package/bundles/antigravity-plugin/scripts/caddis_exit.py +2 -0
  9. package/bundles/antigravity-plugin/scripts/caddis_fleet.py +439 -0
  10. package/bundles/antigravity-plugin/scripts/caddis_frontmatter.py +1 -0
  11. package/bundles/antigravity-plugin/scripts/caddis_gate.py +255 -195
  12. package/bundles/antigravity-plugin/scripts/caddis_glossary.py +3 -3
  13. package/bundles/antigravity-plugin/scripts/caddis_governor.py +48 -4
  14. package/bundles/antigravity-plugin/scripts/caddis_inventory.py +38 -0
  15. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +411 -127
  16. package/bundles/antigravity-plugin/scripts/caddis_lineage_publish.py +458 -0
  17. package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +237 -0
  18. package/bundles/antigravity-plugin/scripts/caddis_notify.py +125 -0
  19. package/bundles/antigravity-plugin/scripts/caddis_prod_check.py +380 -0
  20. package/bundles/antigravity-plugin/scripts/caddis_redact.py +99 -0
  21. package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +2 -2
  22. package/bundles/antigravity-plugin/scripts/caddis_review.py +1309 -0
  23. package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +2109 -0
  24. package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +676 -0
  25. package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +2605 -0
  26. package/bundles/antigravity-plugin/scripts/caddis_routing.py +7 -6
  27. package/bundles/antigravity-plugin/scripts/caddis_rows.py +476 -0
  28. package/bundles/antigravity-plugin/scripts/caddis_spawn.py +8 -9
  29. package/bundles/antigravity-plugin/scripts/caddis_spend.py +415 -0
  30. package/bundles/antigravity-plugin/scripts/caddis_tidy.py +357 -20
  31. package/bundles/antigravity-plugin/scripts/caddis_todo.py +55 -0
  32. package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +178 -19
  33. package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +349 -0
  34. package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +0 -56
  35. package/bundles/antigravity-plugin/scripts/claudster_doctor.py +1 -1
  36. package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +0 -1
  37. package/bundles/antigravity-plugin/scripts/oss_review.py +2 -2
  38. package/bundles/antigravity-plugin/scripts/roadmap-recheck.ps1 +89 -0
  39. package/bundles/antigravity-plugin/scripts/roadmap-run.ps1 +88 -0
  40. package/bundles/antigravity-plugin/scripts/setup_project_ai.py +43 -1
  41. package/bundles/antigravity-plugin/session_end_agy.py +3 -3
  42. package/bundles/antigravity-plugin/skills/_registry.md +4 -6
  43. package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +1 -1
  44. package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +1 -1
  45. package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +1 -1
  46. package/bundles/antigravity-plugin/skills/catchup/SKILL.md +11 -0
  47. package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +1 -1
  48. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +5 -2
  49. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +35 -83
  50. package/bundles/antigravity-plugin/skills/fleet-close/SKILL.md +167 -0
  51. package/bundles/antigravity-plugin/skills/fleet-open/SKILL.md +110 -0
  52. package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +2 -1
  53. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +177 -48
  54. package/bundles/antigravity-plugin/skills/implement/SKILL.md +87 -147
  55. package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +4 -3
  56. package/bundles/antigravity-plugin/skills/mmr/SKILL.md +5 -2
  57. package/bundles/antigravity-plugin/skills/office-documents-official/SKILL.md +24 -0
  58. package/bundles/antigravity-plugin/skills/park/SKILL.md +0 -1
  59. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +28 -20
  60. package/bundles/antigravity-plugin/skills/prd/SKILL.md +2 -10
  61. package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +2 -1
  62. package/bundles/antigravity-plugin/skills/review-pr/SKILL.md +1 -1
  63. package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +260 -0
  64. package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +77 -0
  65. package/bundles/antigravity-plugin/skills/roadmap/assets/render-check.template.mjs +68 -0
  66. package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +78 -0
  67. package/bundles/antigravity-plugin/skills/roadmap/assets/roadmap.template.md +32 -0
  68. package/bundles/antigravity-plugin/skills/roadmap/assets/value-check.template.py +161 -0
  69. package/bundles/antigravity-plugin/skills/ship/SKILL.md +16 -16
  70. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +15 -5
  71. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +36 -16
  72. package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +4 -8
  73. package/bundles/antigravity-plugin/warm_start_agy.py +1 -3
  74. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  75. package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -2
  76. package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
  77. package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +5 -2
  78. package/bundles/antigravity-plugin-extras/skills/runner-move/SKILL.md +214 -0
  79. package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +2 -1
  80. package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +2 -1
  81. package/bundles/codex/.codex/skills/docs/office-documents-official/SKILL.md +24 -0
  82. package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +1 -1
  83. package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +1 -1
  84. package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +1 -1
  85. package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +260 -0
  86. package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +77 -0
  87. package/bundles/codex/.codex/skills/workflow/roadmap/assets/render-check.template.mjs +68 -0
  88. package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +78 -0
  89. package/bundles/codex/.codex/skills/workflow/roadmap/assets/roadmap.template.md +32 -0
  90. package/bundles/codex/.codex/skills/workflow/roadmap/assets/value-check.template.py +161 -0
  91. package/bundles/codex/AGENTS.md +5 -4
  92. package/bundles/manifest.json +3 -3
  93. package/package.json +1 -1
  94. package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +0 -233
  95. package/bundles/antigravity-plugin/skills/advisory-hub/reference/context-template.md +0 -214
  96. package/bundles/antigravity-plugin/skills/advisory-hub/reference/hub-handbook.md +0 -233
  97. package/bundles/antigravity-plugin/skills/advisory-hub/reference/report-contract.md +0 -94
  98. package/bundles/antigravity-plugin/skills/advisory-hub/reference/succession.md +0 -118
  99. package/bundles/antigravity-plugin/skills/resume/SKILL.md +0 -6
  100. package/bundles/antigravity-plugin/skills/review/SKILL.md +0 -6
  101. package/bundles/antigravity-plugin/skills/spawn-hub/SKILL.md +0 -341
  102. package/bundles/antigravity-plugin/skills/statusline/SKILL.md +0 -6
  103. package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +0 -143
  104. package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +0 -158
  105. package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +0 -1430
  106. package/bundles/antigravity-plugin-extras/skills/golden-plan/SKILL.md +0 -586
  107. package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +0 -233
  108. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +0 -214
  109. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +0 -233
  110. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +0 -94
  111. package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +0 -118
@@ -0,0 +1,68 @@
1
+ // Render check template. Copy it into the app (for example frontend/render-check.mjs) so CI and prod checks can run it.
2
+ //
3
+ // Usage:
4
+ // node render-check.mjs --base http://localhost:5173 --path /dashboard --expect kpi-total --expect chart-main
5
+ // node render-check.mjs --prod https://app.example.com --path /dashboard --expect kpi-total --channel msedge \
6
+ // --allow-auth app.example.com
7
+ // node render-check.mjs --base http://localhost:5173 --path /dashboard --expect kpi-total --prove-it-can-fail
8
+ //
9
+ // Options:
10
+ // --base <url> | --prod <url> Origin to test (local or prod). One of them is required.
11
+ // --path <path> Page path. Default "/".
12
+ // --expect <testid> A data-testid that must be visible. Repeat it.
13
+ // --channel <name> Browser channel, for example msedge. Default: bundled chromium.
14
+ // --allow-auth <host> Host that may use the current Windows login (NTLM). Repeat it.
15
+ // --settle <ms> Wait after network idle. Default 3000.
16
+ // --prove-it-can-fail Add a test id that does not exist. The run must then exit 1.
17
+ // Exit codes: 0 all checks pass, 1 a check failed, 4 bad arguments.
18
+ import { chromium } from 'playwright'; // or: import { chromium } from '@playwright/test';
19
+
20
+ const args = process.argv.slice(2);
21
+ const opt = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
22
+ const all = (name) => args.flatMap((a, i) => (a === name ? [args[i + 1]] : []));
23
+
24
+ const origin = opt('--prod') ?? opt('--base');
25
+ if (!origin) { console.error('give --base <url> or --prod <url>'); process.exit(4); }
26
+ const path = opt('--path') ?? '/';
27
+ const settle = Number(opt('--settle') ?? 3000);
28
+ const expected = all('--expect');
29
+ const allowAuth = all('--allow-auth');
30
+ if (args.includes('--prove-it-can-fail')) expected.push('testid-that-does-not-exist');
31
+ if (expected.length === 0) { console.error('give at least one --expect <testid>'); process.exit(4); }
32
+
33
+ const launch = { headless: true };
34
+ if (opt('--channel')) launch.channel = opt('--channel');
35
+ if (allowAuth.length) launch.args = [`--auth-server-allowlist=${allowAuth.join(',')}`];
36
+
37
+ const failures = [];
38
+ const browser = await chromium.launch(launch);
39
+ for (const width of [1440, 400]) {
40
+ const context = await browser.newContext({ viewport: { width, height: 900 } });
41
+ // Dark theme seeded before any page script runs. Change the key and value to match the app.
42
+ await context.addInitScript(() => { try { localStorage.setItem('theme', 'dark'); } catch { /* ignore */ } });
43
+ const page = await context.newPage();
44
+ let consoleErrors = 0;
45
+ page.on('console', (m) => { if (m.type() === 'error') consoleErrors += 1; });
46
+ page.on('pageerror', () => { consoleErrors += 1; });
47
+
48
+ await page.goto(new URL(path, origin).toString());
49
+ await page.waitForLoadState('networkidle');
50
+ await page.waitForTimeout(settle);
51
+
52
+ if (consoleErrors > 0) failures.push(`${width}px: ${consoleErrors} console error(s)`);
53
+ const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth);
54
+ if (overflow) failures.push(`${width}px: horizontal scroll`);
55
+ for (const id of expected) {
56
+ const visible = await page.getByTestId(id).first().isVisible().catch(() => false);
57
+ if (!visible) failures.push(`${width}px: data-testid "${id}" is not visible`);
58
+ }
59
+ console.log(`${width}px: console errors ${consoleErrors}, horizontal scroll ${overflow ? 'yes' : 'no'}`);
60
+ await context.close();
61
+ }
62
+ await browser.close();
63
+
64
+ if (failures.length) {
65
+ for (const f of failures) console.error(`FAIL ${f}`);
66
+ process.exit(1);
67
+ }
68
+ console.log('PASS render check');
@@ -0,0 +1,78 @@
1
+ ---
2
+ type: reference
3
+ title: Review protocol
4
+ description: The review method one model can rely on - invariants, inputs and writers, shared state, execute what the change promises, tests, docs, then severity (only blocking stops the merge; each finding needs a reproduction), one FINDING line per finding and an END REVIEW line. caddis_review.py sends it to every review run.
5
+ status: current
6
+ ---
7
+
8
+ # Review protocol (generic, any diff)
9
+
10
+ You review a diff. You do NOT edit, create or delete files, and you do not commit. You may read files and run commands
11
+ (tests, dry runs, the commands that plans, cards and docs name). Work through ALL steps in order. Do not stop at the
12
+ first finding. Include a short section per step, in order.
13
+
14
+ ## Step 1 - Purpose and invariants
15
+ Read the card, the plan or design section it cites, and the diff. Write the list of INVARIANTS this change must keep:
16
+ safety limits, gates, money caps, isolation between runs or users, path containment, "X can never cause Y". Include
17
+ invariants that the surrounding system relies on, not only the ones the card names.
18
+
19
+ ## Step 2 - Inputs and who controls them
20
+ List every input the changed code reads: files, file sections, env vars, CLI args, other processes' output, the clock,
21
+ the home folder. For EACH input write: who or what can write it (the owner, an unattended session, another runner,
22
+ a test, a template), and when. Then, for each invariant from step 1, ask: can any writer of any input break it, by
23
+ accident or on purpose? This includes the agent or session the code is meant to limit.
24
+
25
+ ## Step 3 - Shared state and concurrency
26
+ List state that two runs, two kinds of target, or two processes could share (files, locks, relays, logs, caches).
27
+ Can they collide? Can one read the other's output as its own?
28
+
29
+ ## Step 4 - Execute what the change promises
30
+ Run the tests the card names. Then run every command that the plan, its exit gates, the docs or the skill text say
31
+ should work for this change (dry runs, the documented usage lines), exactly as written. Use the repo's OWN fixtures
32
+ and example files, not ones you build, and run unattended scripts (hooks, runners) as they are really run (as a script,
33
+ with a real payload). Compare risky behaviour with the parent commit when the change claims to fix or tighten
34
+ something. Report any that fail or that cannot work as documented.
35
+
36
+ ## Step 5 - Tests
37
+ For each new or changed test: could it pass for the wrong reason (no assertion that can fail, mocked away the thing
38
+ under test)? Does it depend on the real machine (home folder, real STOP files, network, clock, cwd)? Which new
39
+ behaviour has no test, especially the negative cases (placeholders, malformed input, edge values)?
40
+
41
+ ## Step 6 - Docs and contracts
42
+ Compare every doc, skill, prompt asset and usage line touched by or describing this feature with the code. List each
43
+ mismatch (paths, flags, names, steps that do not exist or no longer apply). Check that every tool the new mode is told
44
+ to use supports the new mode.
45
+
46
+ ## Step 7 - Report
47
+ Severity rule. Only **blocking** stops the merge, so use it with care:
48
+ - **blocking** = a defect that breaks a stated invariant from step 1 (safety, security, isolation, a gate passing when
49
+ it should fail, a crash of something that runs unattended), or loses or corrupts work or data, or breaks a money cap,
50
+ or makes a documented command fail. Use it even if other limits make the defect unlikely.
51
+ - **should-fix** = a real defect that does not stop the merge: it goes on a follow-up list. Examples: a missing
52
+ negative test, a doc that is wrong in a way that does not make a command fail, an error message that misleads.
53
+ - **nit** = style and wording.
54
+
55
+ Every finding needs a concrete reproduction: a command, an input or a test that shows the failure, written in the
56
+ failure scenario. A finding without a reproduction is a **nit**, whatever its severity would otherwise be. Do not
57
+ write "might", "could in theory" or "a future change may": show it or call it a nit.
58
+
59
+ The gate exits by the findings. A **blocking** finding fails it. **should-fix** and **nit** findings do not: they are
60
+ listed in the report under "Follow-up (not blocking)". The tool allows 3 completed review rounds for a phase; the 4th completed round
61
+ ends with "stop, the owner decides". Report every real defect once, with its true severity, so that one round is
62
+ enough.
63
+
64
+ End your answer with the findings, one line per finding, in exactly this form (no bullets, no bold, no code block):
65
+
66
+ ```
67
+ FINDING <n> | <severity> | <file>:<line> | <one-sentence failure scenario>
68
+ ```
69
+
70
+ `<n>` counts from 1, `<severity>` is `blocking`, `should-fix` or `nit`. Put the reproduction in the one sentence
71
+ (for example `... ; reproduce: python x.py --flag with an empty file`). If you have no findings, write the line
72
+ `NO FINDINGS` instead. The very last line of your answer is `END REVIEW <count>`, where `<count>` is the number of
73
+ FINDING lines (`END REVIEW 0` after `NO FINDINGS`). A review whose last line is missing or whose count is wrong is
74
+ treated as not run. Do not write a verdict line: the gate reads the findings only.
75
+ No other line may contain the word FINDING followed by a number, anywhere in the line (in any case, also as
76
+ `finding-1`, `finding #1`, `finding (1)`, after a bullet, a `>`, a table `|` or any other text): such a line that is
77
+ not a complete FINDING line also makes the review count as not run. In your step sections, write "issue 1", not
78
+ "finding 1".
@@ -0,0 +1,32 @@
1
+ # <slug> roadmap
2
+
3
+ Runner host: HOSTNAME
4
+
5
+ The roadmap file on `origin/main` is the only truth. Change it with `scripts/caddis_roadmap.py` (each change is a small
6
+ docs PR), never by a direct push. Only `rollup` sets a milestone `done`.
7
+
8
+ ## Milestones
9
+ | ID | Milestone | Phases | Depends on | Exit check | Status | Owner | Target |
10
+ |---|---|---|---|---|---|---|---|
11
+ | S1 | <what is true when it is done> | <plan>#1, <plan>#2 | - | `<command that exits 0 when S1 holds>` | todo | runner | <date> |
12
+
13
+ ## Blockers
14
+ | Milestone | Type | Reason | Check |
15
+ |---|---|---|---|
16
+
17
+ ## Notes
18
+
19
+ - Status: todo, in-progress, code-done/data-pending, done or not-needed (blocked = has a Blockers row).
20
+ - Blocker types: owner-gate, external, shared, calendar or data-condition (Check: a command, added by the owner
21
+ only; recheck runs it in a worktree of origin/main and clears that row on exit 0). A calendar blocker's Check is
22
+ an ISO date YYYY-MM-DD; recheck clears the row once that date has passed.
23
+ - Status not-needed needs --reason "dropped: why" or --reason .caddis/parking-lot/file.md; the reason is added
24
+ below as "- id not-needed: reason (date)".
25
+ - status lists each blocker row older than 14 days as "old blockers: id type (N days)"; the age comes from git.
26
+ - A malformed section (wrong cell count, a row without a leading pipe, a line right after the table, a pipe in
27
+ prose, a second table, a renamed heading) stops every command: fix it by a PR. Escape a pipe in prose as \|.
28
+ - This file allows only headings, plain paragraphs, lists and pipe tables: no code fences, no HTML, no setext
29
+ headings, no control or non-ASCII whitespace characters. GitHub would hide or move lines the parser reads, so
30
+ each one is a parse error.
31
+ - Owner: runner (or empty) = the runner may take it; a session name = that session does it by hand.
32
+ - A Depends on change cites a decision id from decisions.md in the Milestone cell: `S9 (dep waived: D4)`.
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ """Value check template: compare figures from an endpoint with an independent source over a fixed past window.
3
+
4
+ Copy it into the app (for example scripts/check_values.py) so CI and prod checks can run it again.
5
+
6
+ python value-check.template.py --base http://localhost:8000 --expected expected.json
7
+ python value-check.template.py --base http://localhost:8000 --expected expected.json --prove-it-can-fail
8
+
9
+ Expected file:
10
+ {"window": {"start": "2026-09-01", "end": "2026-09-30"},
11
+ "figures": [{"name": "...", "endpoint": "/api/x", "path": "a.b", "expected": 1200,
12
+ "source": "where the expected value came from", "population": "who is counted",
13
+ "denominator": "what the rate divides by, or none", "tolerance": 0.001, "series": "day"}]}
14
+
15
+ Rules:
16
+ - The window must end before today. A moving window ("today") gives a number that changes while you check it.
17
+ - Every figure names its source, population and denominator.
18
+ - A "series": "day" figure has ISO date keys that all fall on one weekday. A weekday gap is not a data gap.
19
+ - --prove-it-can-fail changes the expected values on purpose. The run must then exit 1.
20
+
21
+ Exit codes: 0 all figures pass, 1 a figure fails, 4 bad input or an HTTP error that is not a figure failure.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import copy
27
+ import datetime as dt
28
+ import json
29
+ import sys
30
+ import urllib.error
31
+ import urllib.parse
32
+ import urllib.request
33
+
34
+ _MISSING = object()
35
+
36
+
37
+ class BadInput(Exception):
38
+ """The expected file or the request is wrong. Exit 4."""
39
+
40
+
41
+ def _parse_date(text: str, what: str) -> dt.date:
42
+ try:
43
+ return dt.date.fromisoformat(str(text))
44
+ except ValueError as exc:
45
+ raise BadInput(f"{what} is not an ISO date: {text!r}") from exc
46
+
47
+
48
+ def validate(spec: dict) -> None:
49
+ window = spec.get("window") or {}
50
+ start, end = _parse_date(window.get("start"), "window.start"), _parse_date(window.get("end"), "window.end")
51
+ if end >= dt.date.today():
52
+ raise BadInput(f"window.end {end} is not before today ({dt.date.today()}): use a fixed past window")
53
+ if start > end:
54
+ raise BadInput("window.start is after window.end")
55
+ figures = spec.get("figures") or []
56
+ if not figures:
57
+ raise BadInput("no figures in the expected file")
58
+ for fig in figures:
59
+ name = fig.get("name") or "<unnamed>"
60
+ for key in ("name", "endpoint", "path", "expected"):
61
+ if key not in fig or fig[key] in ("", None):
62
+ raise BadInput(f"figure {name!r} has no {key}")
63
+ for key in ("source", "population", "denominator"):
64
+ if not str(fig.get(key, "")).strip():
65
+ raise BadInput(f"figure {name!r} has an empty {key}: name it")
66
+ if fig.get("series") == "day":
67
+ if not isinstance(fig["expected"], dict) or not fig["expected"]:
68
+ raise BadInput(f"figure {name!r}: a day series needs a non-empty object of date keys")
69
+ weekdays = {_parse_date(k, f"figure {name!r} key").weekday() for k in fig["expected"]}
70
+ if len(weekdays) > 1:
71
+ raise BadInput(f"figure {name!r}: day series keys fall on more than one weekday")
72
+
73
+
74
+ def prove_it_can_fail(spec: dict) -> dict:
75
+ """Return a copy with a wrong expected value, so a correct endpoint must fail."""
76
+ spec = copy.deepcopy(spec)
77
+ scalars = [f for f in spec["figures"] if not isinstance(f["expected"], dict)]
78
+ if scalars:
79
+ scalars[0]["expected"] = scalars[0]["expected"] + 1
80
+ series = [f for f in spec["figures"] if isinstance(f["expected"], dict)]
81
+ if series:
82
+ series[0]["expected"].pop(next(iter(series[0]["expected"]))) # a missing date must fail the compare
83
+ series[0]["expected"]["1999-01-04"] = 0 # 1999-01-04 is a Monday; the endpoint has no such key
84
+ if not scalars and not series:
85
+ spec["figures"][0]["path"] += ".missing"
86
+ return spec
87
+
88
+
89
+ def fetch(base: str, endpoint: str, window: dict, headers: dict) -> tuple[int, object]:
90
+ query = urllib.parse.urlencode({"start": window["start"], "end": window["end"]})
91
+ req = urllib.request.Request(f"{base.rstrip('/')}{endpoint}?{query}", headers=headers)
92
+ try:
93
+ with urllib.request.urlopen(req, timeout=30) as resp:
94
+ return resp.status, json.loads(resp.read().decode("utf-8"))
95
+ except urllib.error.HTTPError as exc:
96
+ return exc.code, None
97
+ except (urllib.error.URLError, TimeoutError, ValueError) as exc:
98
+ raise BadInput(f"cannot read {endpoint}: {exc}") from exc
99
+
100
+
101
+ def dig(data: object, path: str) -> object:
102
+ for part in path.split("."):
103
+ if isinstance(data, dict) and part in data:
104
+ data = data[part]
105
+ else:
106
+ return _MISSING
107
+ return data
108
+
109
+
110
+ def same(actual: object, expected: object, tolerance: float) -> bool:
111
+ if isinstance(expected, dict):
112
+ if not isinstance(actual, dict):
113
+ return False
114
+ return all(k in actual and same(actual[k], v, tolerance) for k, v in expected.items())
115
+ if isinstance(expected, bool) or isinstance(actual, bool) or not isinstance(expected, (int, float)):
116
+ return actual == expected
117
+ return isinstance(actual, (int, float)) and abs(actual - expected) <= tolerance
118
+
119
+
120
+ def check(spec: dict, base: str, headers: dict) -> bool:
121
+ ok = True
122
+ for fig in spec["figures"]:
123
+ status, body = fetch(base, fig["endpoint"], spec["window"], headers)
124
+ actual = dig(body, fig["path"]) if status == 200 else _MISSING
125
+ passed = actual is not _MISSING and same(actual, fig["expected"], float(fig.get("tolerance", 0)))
126
+ shown = "missing" if actual is _MISSING else actual
127
+ if status != 200:
128
+ shown = f"HTTP {status}"
129
+ print(f"{'PASS' if passed else 'FAIL'} {fig['name']}: endpoint {shown} vs {fig['source']} {fig['expected']}; "
130
+ f"population {fig['population']}; denominator {fig['denominator']}")
131
+ ok = ok and passed
132
+ return ok
133
+
134
+
135
+ def main(argv: list[str] | None = None) -> int:
136
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
137
+ ap.add_argument("--base", required=True, help="origin of the app, for example http://localhost:8000")
138
+ ap.add_argument("--expected", required=True, help="expected figures JSON file")
139
+ ap.add_argument("--prove-it-can-fail", action="store_true", help="change the expected values; the run must exit 1")
140
+ ap.add_argument("--header", action="append", default=[], metavar="K:V", help="extra request header (repeatable)")
141
+ args = ap.parse_args(argv)
142
+ try:
143
+ with open(args.expected, encoding="utf-8") as fh:
144
+ spec = json.load(fh)
145
+ validate(spec)
146
+ headers = {}
147
+ for h in args.header:
148
+ key, sep, value = h.partition(":")
149
+ if not sep:
150
+ raise BadInput(f"--header needs K:V, got {h!r}")
151
+ headers[key.strip()] = value.strip()
152
+ if args.prove_it_can_fail:
153
+ spec = prove_it_can_fail(spec)
154
+ return 0 if check(spec, args.base, headers) else 1
155
+ except (BadInput, OSError, ValueError) as exc:
156
+ print(f"bad input: {exc}", file=sys.stderr)
157
+ return 4
158
+
159
+
160
+ if __name__ == "__main__":
161
+ sys.exit(main())
@@ -61,7 +61,7 @@ these files are the truth.
61
61
  runbook, analysis, handoff) must open with a YAML frontmatter block:
62
62
  ```yaml
63
63
  ---
64
- type: plan|prd|adr|design|runbook|handoff|analysis|review|prompt|parking-lot|rca|todo|relay|comms|reference|note|phase-report|implement-review|phase-verdict|hub-spawn|advisory-context|phase-prompt
64
+ type: plan|prd|adr|design|runbook|handoff|analysis|review|prompt|parking-lot|rca|todo|relay|comms|reference|note|phase-report|implement-review|phase-verdict|hub-spawn|advisory-context|phase-prompt|open-work
65
65
  status: draft|current|done|superseded
66
66
  feature: <feature-slug>
67
67
  creation-agent: caddis
@@ -114,9 +114,10 @@ future: yes # OPTIONAL. yes = committed. Absent or `no` = a candida
114
114
  `status` and `future` answer different questions and must stay separate: where the item is in its
115
115
  life, and whether we have agreed to do it. `python scripts/caddis_tidy.py --check` and the
116
116
  `caddis_gate.py parking-lot` gate both fail on a non-conforming item, so this is enforced, not
117
- advised. Two neighbours that are NOT the backlog: `/caddis:digress` parks an interrupted task on a
118
- stack (`/caddis:catchup` pops it), and `.caddis/backlog/` is written by **docket** as a projection of
119
- its board — never hand-edit that one.
117
+ advised. `--check` also fails on a plan whose Tracker is all done while its `status:` is still
118
+ active, and with `--commit-range BASE..HEAD` it reports violations only in the files that range
119
+ changed. One neighbour that is NOT the backlog: `/caddis:digress` parks an interrupted task on a
120
+ stack (`/caddis:catchup` pops it).
120
121
 
121
122
  ### Write environment facts down in the same turn
122
123
  **If you learn a fact about the environment, write it to `.caddis/kb/environment-map.md` before you
@@ -1,8 +1,8 @@
1
1
  {
2
- "poolVersion": "1.3.138",
2
+ "poolVersion": "1.3.140",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.138",
5
- "antigravity-plugin-extras": "1.3.42",
4
+ "antigravity-plugin": "1.3.140",
5
+ "antigravity-plugin-extras": "1.3.43",
6
6
  "codex": "unknown"
7
7
  }
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.4.52",
3
+ "version": "0.4.54",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",
@@ -1,233 +0,0 @@
1
- ---
2
- name: advisory-hub
3
- description: >-
4
- A long-lived "Hub" session re-derives each phase of a multi-session plan instead of trusting its
5
- self-report. Use when the user says "advisory hub", "validate this phase", "phase report", "hub
6
- verdict", "who checks the implementing session", "re-derive don't trust", "hand off the hub", or a
7
- long plan with expensive-to-reverse work (migration, security change, production data correctness)
8
- needs independent checks; not for short features. Hub artefacts go in
9
- `.caddis/advisory-hub-reports/`.
10
- ---
11
-
12
- # Advisory Hub — cross-session phase validation
13
-
14
- ## The whole model, in four statements
15
-
16
- > **1. Re-derive, don't trust — and re-derive DIFFERENTLY than the implementer did.**
17
- > **2. Two roles, four files.** The implementer writes only the report. The Hub writes everything else.
18
- > **3. The memory IS the switch.** `<plan-stem>-advisory-context.md` turns the mode on *and* is the
19
- > Hub's brain. An empty file buys a rubber stamp.
20
- > **4. Nothing is fixed until it lands in the context doc.** A correction living in a verdict or a
21
- > chat dies with the session that wrote it.
22
-
23
- ### 1. The rule, and the correction to it
24
-
25
- A phase report is a **self-assessment**. When it says *"VALUE CHECK passed: endpoint 11,126 = SQL
26
- 11,126,"* the Hub **runs the query itself**. A Hub that reads reports and nods is *worse than no Hub*
27
- — it manufactures false confidence.
28
-
29
- **But re-running a gate *identically* is not independent verification** — it reproduces the
30
- implementer's blind spot. That cost a failed production deploy: implementer and Hub both ran the same
31
- suite, both green, and CI then failed on a dependency-injection bug the gitignored local env file had
32
- been masking. **For anything touching config, a data seam, or dependency injection, deprive the
33
- environment:** `mv <env-file> aside && <test command> && mv back`.
34
-
35
- ### 2. Two roles, four files
36
-
37
- | File | Written by | When | What is lost without it |
38
- |---|---|---|---|
39
- | `<plan-stem>-advisory-context.md` | scaffolded at planning; **owned and maintained by the Hub** | before | The Hub's memory — re-derivation access, locked decisions *and their non-obvious reasons*, the disproved-claims list. Nothing else survives a Hub crash or compaction |
40
- | `.caddis/advisory-hub-reports/phase-NN.prompt.md` | **the Hub** | **BEFORE the phase runs** | What the implementer was *told*. Without it, "the implementer deviated" and "the plan was wrong" are both unfalsifiable |
41
- | `.caddis/advisory-hub-reports/phase-NN.report.md` | the implementing session | after | The only evidence a phase's claims were ever checkable rather than merely asserted |
42
- | `.caddis/advisory-hub-reports/phase-NN.verdict.md` | **the Hub** | after | A later reader sees a self-assessment and no evidence anyone checked it |
43
-
44
- **Role separation is not decoration.** An implementer that writes its own verdict has produced a
45
- self-assessment wearing a verdict's name — worse than no verdict, because it *looks* like a check
46
- happened. Diligence is no substitute: an implementer can re-derive its own claims honestly and still
47
- produce a worthless verdict.
48
-
49
- Where a `phase-NN.prompt.md` exists it is **the instruction of record** — `/caddis:implement` reads it,
50
- and deviations are measured against it, not against the plan alone.
51
-
52
- **The launch command does not belong in `phase-NN.prompt.md`.** That file is read by the implementer,
53
- i.e. *after* launch — a file instructing you to spawn GLM is one GLM only reads once it has already
54
- been spawned. Observed on a real plan, and the skill said nothing either way, so it will recur.
55
- Surface the launch command to whoever is **orchestrating** (the human, or the session that will spawn
56
- the phase). What the phase prompt SHOULD carry is a one-line **lane declaration** — *"This phase must
57
- run on `<lane>`"* — so a mis-launched session can self-detect and act on it.
58
-
59
- The lane a phase gets is also **provisional until this file is written.** Writing the phase prompt is
60
- the first time anyone but the spec's author reads the spec; if the phase turns out to need judgment a
61
- no-escalation lane cannot supply, downgrade it here.
62
-
63
- ### 3. The switch
64
-
65
- `<plan-stem>-advisory-context.md`, beside the plan. `/caddis:implement`, `/caddis:handoff` and
66
- `/caddis:validate-phase` all check for it. **No file → zero behaviour change, in every command, ever.**
67
-
68
- Do not `touch` an empty one to "turn it on". The switch and the memory are deliberately the same file:
69
- it should be structurally impossible to enable the Hub without giving it a brain.
70
-
71
- **The verdict gates the next phase, by machine.** In Advisory-Hub mode, before starting phase N,
72
- `/caddis:implement` requires `phase-(N-1).verdict.md` carrying `verdict: accept` or
73
- `accept-with-correction`. Missing, or `reject` → stop and report `blocked-pending-hub`.
74
-
75
- ### 4. Corrections land structurally
76
-
77
- Every correction the Hub issues must be asked: *where does this live so it cannot recur?* Telling the
78
- implementing session is worthless — that session is closing. Putting it in the next phase's prompt
79
- works exactly once. **Put it in the context doc**, where it binds every future phase whether or not
80
- anyone remembers it. Each verdict therefore ends with *"what is now structural."*
81
-
82
- Corollary: **feed the generic form back to this skill's parking lot.** Project facts stay in the
83
- project; role mechanics come back here, or the skill never improves.
84
-
85
- ---
86
-
87
- ## When it earns its keep
88
-
89
- **The cost is a full extra session's context, per phase.** Do not reach for this because it sounds
90
- rigorous — reach for it when a mistake is expensive enough that the cost is cheap by comparison.
91
-
92
- > **REQUIRED: the work is expensive to reverse** — a migration, a security change, production data
93
- > correctness, or numbers published to stakeholders. **No stakes → no Hub, whatever else is true.**
94
- >
95
- > **PLUS at least one of:** the plan has **≥8 phases** · it is implemented across **sessions that will
96
- > not share context**.
97
-
98
- A three-phase feature hits none of this. `/caddis:feature-plan` Step 3b runs this test and **offers**;
99
- it never creates the file silently. This is the test's only home — do not restate the thresholds
100
- elsewhere, or the copies drift.
101
-
102
- ### Failing this test correctly does not mean "no discipline"
103
-
104
- A plan can fail the test above — properly, no Hub needed — and still be multi-phase, multi-session,
105
- and worth structuring. The trap is what happens next.
106
-
107
- The Hub's file conventions are *attractive*. Once one plan in a repo runs in Hub mode, its
108
- `phase-NN.prompt.md` / `phase-NN.report.md` / CONTINUE GATE artefacts are sitting right there as the
109
- most recent example of "how we structure a multi-session plan here." A second, smaller plan copies
110
- them by analogy. It gets the readability. It gets **none** of the safety, because the safety was
111
- never in the file names — it was in the role split and the verdict gate.
112
-
113
- That has happened. A six-phase UI plan borrowed the naming, and an implementing session filed its own
114
- batch report **and then wrote the next batch's prompt in the same session**, with nobody having
115
- looked at the work in between. Nothing was wrong that time. Nothing *checked*, either — and the good
116
- outcome is not evidence the mechanism worked, it is evidence nothing tested it.
117
-
118
- This skill already says why role separation is not decoration:
119
-
120
- > *An implementer that writes its own verdict has produced a self-assessment wearing a verdict's
121
- > name — worse than no verdict, because it looks like a check happened.*
122
-
123
- The same sentence holds with two words changed: **an implementer that writes the next phase's
124
- instructions** has produced a plan wearing an independently-authored prompt's name.
125
-
126
- ### The one lightweight rule — cheap enough to always afford
127
-
128
- > **The session that files a phase's report must not also write the next phase's prompt without an
129
- > independent re-check in between.**
130
-
131
- That is the whole rule. It costs minutes, not a session:
132
-
133
- - re-run the test suite rather than trusting the report's claim that it passed
134
- - grep the source for the report's two or three load-bearing claims
135
- - open one or two of the screenshots it describes, rather than reading its description of them
136
-
137
- No switch file. No dedicated Hub session. No advisory-context doc. If the re-check finds nothing, you
138
- have lost ten minutes; if it finds something, it found it *before* the wrong instruction went into a
139
- fresh session that cannot know any better.
140
-
141
- **State which one you are doing, in the plan.** A plan that borrows Hub file conventions should say
142
- either "full Advisory Hub mode" or "Hub file conventions, lightweight re-check rule, no Hub" —
143
- because a reader six phases later cannot tell the two apart from the artefacts alone. That is the
144
- entire problem this section exists to prevent.
145
-
146
- ## Succession is not the same as a standing validator
147
-
148
- Two things look alike from outside and are not:
149
-
150
- - **`/caddis:spawn-hub` is SUCCESSION.** The outgoing Hub **ends**. It hands the role to a fresh
151
- session and stops. There is one Hub afterwards, not two.
152
- - **`/caddis:validate-phase` assumes a Hub that PERSISTS** across phases, checking each one.
153
-
154
- Conflating them builds a review loop that has to be unwound. One session kept Hub-1 alive to check
155
- Hub-2, then dismantled the arrangement when the user said they were closing Hub-1. Both models are
156
- reasonable; running both at once is not.
157
-
158
- ## Every Hub artefact has one home
159
-
160
- `.caddis/advisory-hub-reports/`. A succession prompt is `hub-NN.spawn.md` there, with frontmatter
161
- `type: hub-spawn`. **This holds even when you write one by hand** rather than running
162
- `/caddis:spawn-hub` — and by hand is exactly when it gets missed, because the instruction lives
163
- inside the command you skipped.
164
-
165
- The knock-on is the part that bites. `spawn-hub` rotates its comprehension trap by reading the last
166
- spawn prompt from that directory. A prompt filed anywhere else is invisible to it, so rotation
167
- silently runs against an empty set — the failure the artefact file was introduced to prevent,
168
- returning through a different door. `check_doc_coverage.py` now warns on a `type: hub-spawn` file
169
- found outside that directory.
170
-
171
- ## The Hub is a ROLE, not a conversation
172
-
173
- It outlives any one session and hands over through **files**, never chat history. Hand over with
174
- `/caddis:spawn-hub` — at a milestone boundary, or sooner if context is filling.
175
-
176
- **Your instinct that the context doc is current is measured-wrong.** Asked *"can we hand off?"*, an
177
- outgoing Hub's honest first answer was yes; a mechanical audit then found **seven gaps** — including
178
- that the single highest-value technique then in use was written down nowhere a fresh Hub would find it.
179
-
180
- ## While you hold the role — maintain `relay.md`, not just the succession table
181
-
182
- **The succession table records WHO HELD THE ROLE. `.caddis/relay.md` records WHERE THE WORK IS. A
183
- Hub maintains both; they are not substitutes.**
184
-
185
- The succession table is written when a Hub **ends**. Nothing else covers the hours while a Hub is
186
- **working** — and that is the whole period this mechanism exists to protect, because its entire
187
- purpose is surviving session death.
188
-
189
- > **Mirror your working list into `.caddis/relay.md` as you go.** Your in-session task list does not
190
- > survive this session. `relay.md` does. Anything not written there is lost when the window closes.
191
-
192
- This gap was real. A spawned Hub read `relay.md`, answered a thirteen-question context check
193
- correctly, found four genuine defects in its handover — **and created no tracker at all**, because
194
- nothing asked it to. The user noticed before the agent did: *"I don't see it creating any todo list
195
- yet."* The work was only recoverable because the OUTGOING Hub had written a tracker into `relay.md`
196
- an hour earlier, on its own initiative.
197
-
198
- **`relay.md` is gitignored and machine-local.** That is deliberate — it is per-machine session state,
199
- not a shared artefact — but it has two consequences a Hub must plan around. It never reaches another
200
- machine, so anything a *successor on a different box* needs belongs in the advisory context or the
201
- spawn prompt instead. And no gate can check it through git, so keeping it current is a discipline,
202
- not something a build will catch for you.
203
-
204
- ## Where the detail lives
205
-
206
- Each file below is loaded by a **named command step**, at the moment it is needed. This is not optional
207
- reading — it is deferred reading.
208
-
209
- | Load this | For | Loaded by |
210
- |---|---|---|
211
- | `reference/context-template.md` | scaffolding or maintaining the context doc | `feature-plan` 3b · `validate-phase` 0 · `spawn-hub` 3 |
212
- | `reference/report-contract.md` | filing a phase report | `implement`, at report time |
213
- | `reference/hub-handbook.md` | validating a phase — verdict template, checklist, techniques, plan drift | `validate-phase` 2 |
214
- | `reference/succession.md` | handing the role over — audit, self-check trap, carried-open ledger | `spawn-hub` · `handoff` 2b |
215
-
216
- Naming, frontmatter and directory rules ship as an `AGENTS.md` **inside**
217
- `.caddis/advisory-hub-reports/`, so they bind every agent that writes there — including agents that
218
- never loaded this skill.
219
-
220
- ## Not to be confused with
221
-
222
- `.github/prompts/advisory-hub.prompt.md` and `.github/instructions/advisory-mode.instructions.md` use
223
- the same name for an older, unrelated Copilot chat-mode pipeline. It never ships to the Claude Code
224
- plugin.
225
-
226
- ## Anti-patterns
227
-
228
- - **Rubber-stamping.** A verdict with no re-derivation is not a verdict.
229
- - **Re-running a gate the same way and calling it independent verification.** Change the environment.
230
- - **An implementer writing its own verdict.** The roles exist for exactly this.
231
- - **An empty context doc.** The switch *is* the memory.
232
- - **Applying this to a short feature.** State the cost, every time, before adopting it.
233
- - **A correction that lives only in a verdict file.** It dies with the session that wrote it.