@caddis/cli 0.4.52 → 0.4.53
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.
- package/bundles/antigravity-plugin/CADDIS-GLOSSARY.md +1 -0
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +132 -5
- package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +223 -0
- package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +284 -0
- package/bundles/antigravity-plugin/scripts/caddis_exit.py +2 -0
- package/bundles/antigravity-plugin/scripts/caddis_frontmatter.py +1 -0
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +226 -195
- package/bundles/antigravity-plugin/scripts/caddis_glossary.py +3 -3
- package/bundles/antigravity-plugin/scripts/caddis_governor.py +48 -4
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +38 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +294 -127
- package/bundles/antigravity-plugin/scripts/caddis_lineage_publish.py +458 -0
- package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +237 -0
- package/bundles/antigravity-plugin/scripts/caddis_notify.py +121 -0
- package/bundles/antigravity-plugin/scripts/caddis_prod_check.py +380 -0
- package/bundles/antigravity-plugin/scripts/caddis_redact.py +99 -0
- package/bundles/antigravity-plugin/scripts/caddis_relay_git.py +2 -2
- package/bundles/antigravity-plugin/scripts/caddis_review.py +1309 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +2056 -0
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +2572 -0
- package/bundles/antigravity-plugin/scripts/caddis_routing.py +3 -2
- package/bundles/antigravity-plugin/scripts/caddis_rows.py +476 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +8 -9
- package/bundles/antigravity-plugin/scripts/caddis_spend.py +415 -0
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +357 -20
- package/bundles/antigravity-plugin/scripts/caddis_todo.py +55 -0
- package/bundles/antigravity-plugin/scripts/caddis_workstreams.py +178 -19
- package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +349 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +0 -56
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +1 -1
- package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +0 -1
- package/bundles/antigravity-plugin/scripts/oss_review.py +2 -2
- package/bundles/antigravity-plugin/scripts/roadmap-recheck.ps1 +89 -0
- package/bundles/antigravity-plugin/scripts/roadmap-run.ps1 +88 -0
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +43 -1
- package/bundles/antigravity-plugin/session_end_agy.py +3 -3
- package/bundles/antigravity-plugin/skills/_registry.md +2 -6
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/antigravity-plugin/skills/catchup/SKILL.md +7 -0
- package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +29 -83
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +77 -48
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +87 -147
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/mmr/SKILL.md +5 -2
- package/bundles/antigravity-plugin/skills/office-documents-official/SKILL.md +24 -0
- package/bundles/antigravity-plugin/skills/park/SKILL.md +0 -1
- package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +8 -14
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +2 -10
- package/bundles/antigravity-plugin/skills/roadmap/SKILL.md +259 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/review-protocol.md +78 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/antigravity-plugin/skills/roadmap/assets/value-check.template.py +161 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +14 -14
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +6 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +33 -14
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +4 -8
- package/bundles/antigravity-plugin/warm_start_agy.py +1 -3
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -2
- package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
- package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +5 -2
- package/bundles/antigravity-plugin-extras/skills/runner-move/SKILL.md +214 -0
- package/bundles/codex/.codex/skills/docs/office-documents-official/SKILL.md +24 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +1 -1
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +1 -1
- package/bundles/codex/.codex/skills/workflow/roadmap/SKILL.md +259 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/plan-resume-prompt.md +77 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/render-check.template.mjs +68 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/review-protocol.md +78 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/roadmap.template.md +32 -0
- package/bundles/codex/.codex/skills/workflow/roadmap/assets/value-check.template.py +161 -0
- package/bundles/codex/AGENTS.md +5 -4
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
- package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/context-template.md +0 -214
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/report-contract.md +0 -94
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/succession.md +0 -118
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/review/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/spawn-hub/SKILL.md +0 -341
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +0 -6
- package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +0 -143
- package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +0 -158
- package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +0 -1430
- package/bundles/antigravity-plugin-extras/skills/golden-plan/SKILL.md +0 -586
- package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +0 -214
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +0 -233
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +0 -94
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +0 -118
|
@@ -25,8 +25,9 @@ DEFAULT_ROUTING: dict[str, list[str]] = {
|
|
|
25
25
|
"review": ["glm", "deepseek"],
|
|
26
26
|
"consult": ["agy", "glm", "codex"],
|
|
27
27
|
"ci-watch": ["agy"],
|
|
28
|
+
"mechanical": ["haiku"],
|
|
28
29
|
}
|
|
29
|
-
KNOWN_MODELS = frozenset({"opus", "sonnet", "agy", "codex", "glm", "deepseek", "claude"})
|
|
30
|
+
KNOWN_MODELS = frozenset({"opus", "sonnet", "haiku", "agy", "codex", "glm", "deepseek", "claude"})
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class RoutingConfigError(Exception):
|
|
@@ -69,7 +70,7 @@ def resolve(
|
|
|
69
70
|
configured.update(_routing_from(Path(repo) / ".caddis" / "config.toml"))
|
|
70
71
|
|
|
71
72
|
providers = set(oss_model.configured_providers(dict(env)))
|
|
72
|
-
available = {"opus", "sonnet", "claude"} | (providers & {"glm", "deepseek"})
|
|
73
|
+
available = {"opus", "sonnet", "haiku", "claude"} | (providers & {"glm", "deepseek"})
|
|
73
74
|
for tool in ("agy", "codex"):
|
|
74
75
|
if which(tool):
|
|
75
76
|
available.add(tool)
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""Set open-work table cells by row id and column name; archive done rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import caddis_exit
|
|
16
|
+
except ModuleNotFoundError as exc:
|
|
17
|
+
if exc.name != "caddis_exit":
|
|
18
|
+
raise
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "claude-harness" / "scripts"))
|
|
21
|
+
import caddis_exit
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Table:
|
|
26
|
+
heading: str | None
|
|
27
|
+
header_line_idx: int
|
|
28
|
+
separator_line_idx: int
|
|
29
|
+
data_line_indices: list[int]
|
|
30
|
+
columns: list[str]
|
|
31
|
+
id_col_idx: int | None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def split_cells(line: str) -> list[str]:
|
|
35
|
+
"""Return stripped cells of one markdown table line, splitting on unescaped pipes."""
|
|
36
|
+
s = line.strip()
|
|
37
|
+
if not s:
|
|
38
|
+
return []
|
|
39
|
+
parts = re.split(r"(?<!\\)\|", s)
|
|
40
|
+
if parts and parts[0] == "":
|
|
41
|
+
parts.pop(0)
|
|
42
|
+
if parts and parts[-1] == "":
|
|
43
|
+
parts.pop(-1)
|
|
44
|
+
return [p.strip() for p in parts]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_separator(line: str) -> bool:
|
|
48
|
+
cells = split_cells(line)
|
|
49
|
+
if not cells:
|
|
50
|
+
return False
|
|
51
|
+
for cell in cells:
|
|
52
|
+
c = cell.strip()
|
|
53
|
+
if not c:
|
|
54
|
+
return False
|
|
55
|
+
if any(ch not in "-: " for ch in c):
|
|
56
|
+
return False
|
|
57
|
+
if "-" not in c:
|
|
58
|
+
return False
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def find_tables(lines: list[str]) -> list[Table]:
|
|
63
|
+
tables: list[Table] = []
|
|
64
|
+
n = len(lines)
|
|
65
|
+
i = 0
|
|
66
|
+
while i < n:
|
|
67
|
+
if lines[i].startswith("|"):
|
|
68
|
+
run_start = i
|
|
69
|
+
while i < n and lines[i].startswith("|"):
|
|
70
|
+
i += 1
|
|
71
|
+
run_end = i
|
|
72
|
+
run_lines = lines[run_start:run_end]
|
|
73
|
+
if len(run_lines) >= 2 and _is_separator(run_lines[1]):
|
|
74
|
+
header_idx = run_start
|
|
75
|
+
sep_idx = run_start + 1
|
|
76
|
+
data_indices = list(range(run_start + 2, run_end))
|
|
77
|
+
columns = split_cells(lines[header_idx])
|
|
78
|
+
|
|
79
|
+
heading = None
|
|
80
|
+
for k in range(run_start - 1, -1, -1):
|
|
81
|
+
s = lines[k].strip()
|
|
82
|
+
if s.startswith("#"):
|
|
83
|
+
heading = s.lstrip("#").strip()
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
id_col_idx = None
|
|
87
|
+
for col_idx, col in enumerate(columns):
|
|
88
|
+
if col.lower() == "id":
|
|
89
|
+
id_col_idx = col_idx
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
tables.append(
|
|
93
|
+
Table(
|
|
94
|
+
heading=heading,
|
|
95
|
+
header_line_idx=header_idx,
|
|
96
|
+
separator_line_idx=sep_idx,
|
|
97
|
+
data_line_indices=data_indices,
|
|
98
|
+
columns=columns,
|
|
99
|
+
id_col_idx=id_col_idx,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
i += 1
|
|
104
|
+
return tables
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _atomic_write_bytes(target: Path, data: bytes) -> None:
|
|
108
|
+
target = target.resolve()
|
|
109
|
+
temp_path: Path | None = None
|
|
110
|
+
try:
|
|
111
|
+
with tempfile.NamedTemporaryFile(
|
|
112
|
+
dir=target.parent, prefix=f".{target.name}.", suffix=".tmp", delete=False
|
|
113
|
+
) as f:
|
|
114
|
+
temp_path = Path(f.name)
|
|
115
|
+
f.write(data)
|
|
116
|
+
f.flush()
|
|
117
|
+
os.fsync(f.fileno())
|
|
118
|
+
os.replace(temp_path, target)
|
|
119
|
+
temp_path = None
|
|
120
|
+
finally:
|
|
121
|
+
if temp_path is not None and temp_path.exists():
|
|
122
|
+
try:
|
|
123
|
+
temp_path.unlink()
|
|
124
|
+
except OSError:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _norm_id(cell: str) -> str:
|
|
129
|
+
return cell.replace("**", "").replace("__", "").replace("`", "").strip()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def handle_set(args: argparse.Namespace) -> int:
|
|
133
|
+
if not args.id and not args.from_file:
|
|
134
|
+
print("caddis-rows: either --id or --from is required", file=sys.stderr)
|
|
135
|
+
return caddis_exit.ERROR
|
|
136
|
+
if args.id and not args.col:
|
|
137
|
+
print("caddis-rows: --id requires at least one --col", file=sys.stderr)
|
|
138
|
+
return caddis_exit.ERROR
|
|
139
|
+
if args.id and args.from_file:
|
|
140
|
+
print("caddis-rows: cannot specify both --id and --from", file=sys.stderr)
|
|
141
|
+
return caddis_exit.ERROR
|
|
142
|
+
|
|
143
|
+
change_specs: list[dict] = []
|
|
144
|
+
if args.id:
|
|
145
|
+
cols_dict: dict[str, str] = {}
|
|
146
|
+
for c in args.col:
|
|
147
|
+
if "=" not in c:
|
|
148
|
+
print(f"caddis-rows: invalid --col format '{c}', expected NAME=VALUE", file=sys.stderr)
|
|
149
|
+
return caddis_exit.ERROR
|
|
150
|
+
name, val = c.split("=", 1)
|
|
151
|
+
name = name.strip()
|
|
152
|
+
cols_dict[name] = val
|
|
153
|
+
change_specs.append({"id": args.id, "table": args.table, "cols": cols_dict})
|
|
154
|
+
else:
|
|
155
|
+
from_path = Path(args.from_file)
|
|
156
|
+
if from_path.is_file():
|
|
157
|
+
try:
|
|
158
|
+
content = from_path.read_text(encoding="utf-8")
|
|
159
|
+
loaded = json.loads(content)
|
|
160
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
161
|
+
print(f"caddis-rows: failed to read --from file: {exc}", file=sys.stderr)
|
|
162
|
+
return caddis_exit.ERROR
|
|
163
|
+
else:
|
|
164
|
+
try:
|
|
165
|
+
loaded = json.loads(args.from_file)
|
|
166
|
+
except json.JSONDecodeError as exc:
|
|
167
|
+
print(f"caddis-rows: invalid JSON in --from: {exc}", file=sys.stderr)
|
|
168
|
+
return caddis_exit.ERROR
|
|
169
|
+
|
|
170
|
+
if not isinstance(loaded, list):
|
|
171
|
+
print("caddis-rows: --from JSON must be a list of change objects", file=sys.stderr)
|
|
172
|
+
return caddis_exit.ERROR
|
|
173
|
+
|
|
174
|
+
for item in loaded:
|
|
175
|
+
if not isinstance(item, dict) or "id" not in item or "cols" not in item or not isinstance(item["cols"], dict):
|
|
176
|
+
print(f"caddis-rows: invalid item in --from: {item}", file=sys.stderr)
|
|
177
|
+
return caddis_exit.ERROR
|
|
178
|
+
tbl = item.get("table") or args.table
|
|
179
|
+
change_specs.append({"id": item["id"], "table": tbl, "cols": dict(item["cols"])})
|
|
180
|
+
|
|
181
|
+
for spec in change_specs:
|
|
182
|
+
for val in spec["cols"].values():
|
|
183
|
+
val_str = str(val)
|
|
184
|
+
if "\n" in val_str or "\r" in val_str:
|
|
185
|
+
print("caddis-rows: value contains newline characters", file=sys.stderr)
|
|
186
|
+
return caddis_exit.ERROR
|
|
187
|
+
|
|
188
|
+
file_path = Path(args.file)
|
|
189
|
+
if not file_path.exists():
|
|
190
|
+
print(f"caddis-rows: file not found: {file_path}", file=sys.stderr)
|
|
191
|
+
return caddis_exit.BLOCKED
|
|
192
|
+
|
|
193
|
+
raw = file_path.read_bytes()
|
|
194
|
+
text = raw.decode("utf-8")
|
|
195
|
+
crlf = "\r\n" in text
|
|
196
|
+
newline = "\r\n" if crlf else "\n"
|
|
197
|
+
ends_with_newline = text.endswith(newline)
|
|
198
|
+
if ends_with_newline:
|
|
199
|
+
content = text[:-len(newline)]
|
|
200
|
+
else:
|
|
201
|
+
content = text
|
|
202
|
+
lines = content.split(newline) if content else []
|
|
203
|
+
|
|
204
|
+
tables = find_tables(lines)
|
|
205
|
+
|
|
206
|
+
# Validate all changes
|
|
207
|
+
validated_changes: list[dict] = []
|
|
208
|
+
for spec in change_specs:
|
|
209
|
+
item_id = spec["id"]
|
|
210
|
+
table_filter = spec.get("table")
|
|
211
|
+
cols = spec["cols"]
|
|
212
|
+
|
|
213
|
+
candidate_tables = [t for t in tables if t.id_col_idx is not None]
|
|
214
|
+
if not candidate_tables:
|
|
215
|
+
print(
|
|
216
|
+
f"caddis-rows: {file_path}: no table has a column named ID; an open-work table needs | ID | ... | Status |",
|
|
217
|
+
file=sys.stderr,
|
|
218
|
+
)
|
|
219
|
+
return caddis_exit.BLOCKED
|
|
220
|
+
if table_filter is not None:
|
|
221
|
+
candidate_tables = [t for t in candidate_tables if t.heading == table_filter]
|
|
222
|
+
|
|
223
|
+
candidates: list[tuple[Table, int, list[str]]] = []
|
|
224
|
+
for t in candidate_tables:
|
|
225
|
+
for line_idx in t.data_line_indices:
|
|
226
|
+
cells = split_cells(lines[line_idx])
|
|
227
|
+
if len(cells) > t.id_col_idx and _norm_id(cells[t.id_col_idx]) == _norm_id(item_id):
|
|
228
|
+
candidates.append((t, line_idx, cells))
|
|
229
|
+
|
|
230
|
+
if len(candidates) == 0:
|
|
231
|
+
print(f"caddis-rows: row id '{item_id}' not found", file=sys.stderr)
|
|
232
|
+
return caddis_exit.BLOCKED
|
|
233
|
+
|
|
234
|
+
if len(candidates) > 1:
|
|
235
|
+
headings = [c[0].heading for c in candidates]
|
|
236
|
+
print(f"caddis-rows: row id '{item_id}' found in multiple tables: {headings}", file=sys.stderr)
|
|
237
|
+
return caddis_exit.BLOCKED
|
|
238
|
+
|
|
239
|
+
cand_table, cand_line_idx, cand_cells = candidates[0]
|
|
240
|
+
|
|
241
|
+
col_lookup = {col.lower(): idx for idx, col in enumerate(cand_table.columns)}
|
|
242
|
+
for col_name in cols:
|
|
243
|
+
if col_name.lower() not in col_lookup:
|
|
244
|
+
print(f"caddis-rows: column '{col_name}' not found in table '{cand_table.heading}'", file=sys.stderr)
|
|
245
|
+
return caddis_exit.BLOCKED
|
|
246
|
+
|
|
247
|
+
validated_changes.append({
|
|
248
|
+
"id": item_id,
|
|
249
|
+
"table": cand_table,
|
|
250
|
+
"line_idx": cand_line_idx,
|
|
251
|
+
"cols": cols,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
# Apply changes
|
|
255
|
+
row_cells: dict[int, list[str]] = {}
|
|
256
|
+
original_cells: dict[int, list[str]] = {}
|
|
257
|
+
applied_notifications: list[str] = []
|
|
258
|
+
|
|
259
|
+
for v in validated_changes:
|
|
260
|
+
cand_table: Table = v["table"]
|
|
261
|
+
line_idx: int = v["line_idx"]
|
|
262
|
+
cols: dict[str, str] = v["cols"]
|
|
263
|
+
item_id: str = v["id"]
|
|
264
|
+
|
|
265
|
+
if line_idx not in row_cells:
|
|
266
|
+
cells = split_cells(lines[line_idx])
|
|
267
|
+
if len(cells) < len(cand_table.columns):
|
|
268
|
+
cells.extend([""] * (len(cand_table.columns) - len(cells)))
|
|
269
|
+
row_cells[line_idx] = cells
|
|
270
|
+
original_cells[line_idx] = list(cells)
|
|
271
|
+
|
|
272
|
+
current_cells = row_cells[line_idx]
|
|
273
|
+
col_lookup = {col.lower(): idx for idx, col in enumerate(cand_table.columns)}
|
|
274
|
+
|
|
275
|
+
for col_name, raw_val in cols.items():
|
|
276
|
+
col_idx = col_lookup[col_name.lower()]
|
|
277
|
+
canonical_col_name = cand_table.columns[col_idx]
|
|
278
|
+
escaped_val = re.sub(r"(?<!\\)\|", r"\|", str(raw_val))
|
|
279
|
+
if current_cells[col_idx] != escaped_val:
|
|
280
|
+
current_cells[col_idx] = escaped_val
|
|
281
|
+
applied_notifications.append(f"set {item_id} {canonical_col_name}={raw_val}")
|
|
282
|
+
|
|
283
|
+
any_changed = False
|
|
284
|
+
for line_idx, cells in row_cells.items():
|
|
285
|
+
if cells != original_cells[line_idx]:
|
|
286
|
+
lines[line_idx] = "| " + " | ".join(cells) + " |"
|
|
287
|
+
any_changed = True
|
|
288
|
+
|
|
289
|
+
if any_changed:
|
|
290
|
+
output_text = newline.join(lines)
|
|
291
|
+
if ends_with_newline and lines:
|
|
292
|
+
output_text += newline
|
|
293
|
+
_atomic_write_bytes(file_path, output_text.encode("utf-8"))
|
|
294
|
+
|
|
295
|
+
for note in applied_notifications:
|
|
296
|
+
print(note)
|
|
297
|
+
|
|
298
|
+
return caddis_exit.CLEAN
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def handle_archive_done(args: argparse.Namespace) -> int:
|
|
302
|
+
file_path = Path(args.file)
|
|
303
|
+
if not file_path.exists():
|
|
304
|
+
print(f"caddis-rows: file not found: {file_path}", file=sys.stderr)
|
|
305
|
+
return caddis_exit.BLOCKED
|
|
306
|
+
|
|
307
|
+
raw = file_path.read_bytes()
|
|
308
|
+
text = raw.decode("utf-8")
|
|
309
|
+
crlf = "\r\n" in text
|
|
310
|
+
newline = "\r\n" if crlf else "\n"
|
|
311
|
+
ends_with_newline = text.endswith(newline)
|
|
312
|
+
if ends_with_newline:
|
|
313
|
+
content = text[:-len(newline)]
|
|
314
|
+
else:
|
|
315
|
+
content = text
|
|
316
|
+
lines = content.split(newline) if content else []
|
|
317
|
+
|
|
318
|
+
tables = find_tables(lines)
|
|
319
|
+
open_heading = args.open
|
|
320
|
+
closed_heading = args.closed
|
|
321
|
+
|
|
322
|
+
open_table = None
|
|
323
|
+
closed_table = None
|
|
324
|
+
for t in tables:
|
|
325
|
+
if t.heading == open_heading and open_table is None:
|
|
326
|
+
open_table = t
|
|
327
|
+
if t.heading == closed_heading and closed_table is None:
|
|
328
|
+
closed_table = t
|
|
329
|
+
|
|
330
|
+
if open_table is None:
|
|
331
|
+
print(f"caddis-rows: open table '{open_heading}' not found", file=sys.stderr)
|
|
332
|
+
return caddis_exit.BLOCKED
|
|
333
|
+
|
|
334
|
+
if closed_table is None:
|
|
335
|
+
print(f"caddis-rows: closed table '{closed_heading}' not found", file=sys.stderr)
|
|
336
|
+
return caddis_exit.BLOCKED
|
|
337
|
+
|
|
338
|
+
if open_table is closed_table:
|
|
339
|
+
print("caddis-rows: open table and closed table must be distinct", file=sys.stderr)
|
|
340
|
+
return caddis_exit.BLOCKED
|
|
341
|
+
|
|
342
|
+
open_col_map = {c.lower(): idx for idx, c in enumerate(open_table.columns)}
|
|
343
|
+
if "status" not in open_col_map:
|
|
344
|
+
print(f"caddis-rows: open table '{open_heading}' has no Status column", file=sys.stderr)
|
|
345
|
+
return caddis_exit.BLOCKED
|
|
346
|
+
|
|
347
|
+
status_idx = open_col_map["status"]
|
|
348
|
+
archivable_pattern = re.compile(r"^(done|not-needed)\b")
|
|
349
|
+
|
|
350
|
+
archivable_indices: list[int] = []
|
|
351
|
+
archived_ids: list[str] = []
|
|
352
|
+
|
|
353
|
+
for line_idx in open_table.data_line_indices:
|
|
354
|
+
cells = split_cells(lines[line_idx])
|
|
355
|
+
status_val = cells[status_idx].strip().lower() if len(cells) > status_idx else ""
|
|
356
|
+
if archivable_pattern.search(status_val):
|
|
357
|
+
archivable_indices.append(line_idx)
|
|
358
|
+
row_id = (
|
|
359
|
+
cells[open_table.id_col_idx]
|
|
360
|
+
if (open_table.id_col_idx is not None and len(cells) > open_table.id_col_idx)
|
|
361
|
+
else ""
|
|
362
|
+
)
|
|
363
|
+
archived_ids.append(row_id)
|
|
364
|
+
|
|
365
|
+
if not archivable_indices:
|
|
366
|
+
return caddis_exit.CLEAN
|
|
367
|
+
|
|
368
|
+
same_headers = (open_table.columns == closed_table.columns)
|
|
369
|
+
new_closed_lines: list[str] = []
|
|
370
|
+
|
|
371
|
+
if same_headers:
|
|
372
|
+
for idx in archivable_indices:
|
|
373
|
+
new_closed_lines.append(lines[idx])
|
|
374
|
+
else:
|
|
375
|
+
closed_col_map = {c.lower(): idx for idx, c in enumerate(closed_table.columns)}
|
|
376
|
+
dropped = [col for col in open_table.columns if col.lower() not in closed_col_map]
|
|
377
|
+
if dropped:
|
|
378
|
+
print(f"dropped columns: {', '.join(dropped)}")
|
|
379
|
+
|
|
380
|
+
for idx in archivable_indices:
|
|
381
|
+
cells = split_cells(lines[idx])
|
|
382
|
+
open_vals = {
|
|
383
|
+
col.lower(): (cells[i] if i < len(cells) else "")
|
|
384
|
+
for i, col in enumerate(open_table.columns)
|
|
385
|
+
}
|
|
386
|
+
row_cells = []
|
|
387
|
+
for col in closed_table.columns:
|
|
388
|
+
val = open_vals.get(col.lower())
|
|
389
|
+
if val is None or val == "":
|
|
390
|
+
val = "-"
|
|
391
|
+
row_cells.append(val)
|
|
392
|
+
new_closed_lines.append("| " + " | ".join(row_cells) + " |")
|
|
393
|
+
|
|
394
|
+
archivable_set = set(archivable_indices)
|
|
395
|
+
insert_after_idx = (
|
|
396
|
+
closed_table.data_line_indices[-1]
|
|
397
|
+
if closed_table.data_line_indices
|
|
398
|
+
else closed_table.separator_line_idx
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
result_lines: list[str] = []
|
|
402
|
+
for idx, line in enumerate(lines):
|
|
403
|
+
if idx not in archivable_set:
|
|
404
|
+
result_lines.append(line)
|
|
405
|
+
if idx == insert_after_idx:
|
|
406
|
+
result_lines.extend(new_closed_lines)
|
|
407
|
+
|
|
408
|
+
output_text = newline.join(result_lines)
|
|
409
|
+
if ends_with_newline and result_lines:
|
|
410
|
+
output_text += newline
|
|
411
|
+
_atomic_write_bytes(file_path, output_text.encode("utf-8"))
|
|
412
|
+
|
|
413
|
+
for rid in archived_ids:
|
|
414
|
+
print(f"archived {rid}")
|
|
415
|
+
|
|
416
|
+
return caddis_exit.CLEAN
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main(argv: list[str] | None = None) -> int:
|
|
420
|
+
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
|
421
|
+
try:
|
|
422
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
if sys.stderr and hasattr(sys.stderr, "reconfigure"):
|
|
426
|
+
try:
|
|
427
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
428
|
+
except Exception:
|
|
429
|
+
pass
|
|
430
|
+
|
|
431
|
+
if argv is None:
|
|
432
|
+
argv = sys.argv[1:]
|
|
433
|
+
|
|
434
|
+
parser = argparse.ArgumentParser(prog="caddis_rows", exit_on_error=False)
|
|
435
|
+
subparsers = parser.add_subparsers(dest="subcommand")
|
|
436
|
+
|
|
437
|
+
set_parser = subparsers.add_parser("set", exit_on_error=False)
|
|
438
|
+
set_parser.add_argument("--file", required=True)
|
|
439
|
+
set_parser.add_argument("--id")
|
|
440
|
+
set_parser.add_argument("--col", action="append", default=[])
|
|
441
|
+
set_parser.add_argument("--from", dest="from_file")
|
|
442
|
+
set_parser.add_argument("--table")
|
|
443
|
+
|
|
444
|
+
archive_parser = subparsers.add_parser("archive-done", exit_on_error=False)
|
|
445
|
+
archive_parser.add_argument("--file", required=True)
|
|
446
|
+
archive_parser.add_argument("--open", default="Open")
|
|
447
|
+
archive_parser.add_argument("--closed", default="Closed")
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
args = parser.parse_args(argv)
|
|
451
|
+
except (argparse.ArgumentError, argparse.ArgumentTypeError) as exc:
|
|
452
|
+
print(f"caddis-rows: {exc}", file=sys.stderr)
|
|
453
|
+
return caddis_exit.ERROR
|
|
454
|
+
except SystemExit as exc:
|
|
455
|
+
code = exc.code if isinstance(exc.code, int) else 0
|
|
456
|
+
return caddis_exit.ERROR if code != 0 else 0
|
|
457
|
+
|
|
458
|
+
if not args.subcommand:
|
|
459
|
+
print("caddis-rows: subcommand required (set, archive-done)", file=sys.stderr)
|
|
460
|
+
return caddis_exit.ERROR
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
if args.subcommand == "set":
|
|
464
|
+
return handle_set(args)
|
|
465
|
+
elif args.subcommand == "archive-done":
|
|
466
|
+
return handle_archive_done(args)
|
|
467
|
+
else:
|
|
468
|
+
print(f"caddis-rows: unknown subcommand '{args.subcommand}'", file=sys.stderr)
|
|
469
|
+
return caddis_exit.ERROR
|
|
470
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
471
|
+
print(f"caddis-rows: {exc}", file=sys.stderr)
|
|
472
|
+
return caddis_exit.BLOCKED
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
if __name__ == "__main__":
|
|
476
|
+
raise SystemExit(main())
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""caddis_spawn — shared machinery for handing work to a fresh session.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`/caddis:spawn-
|
|
7
|
-
|
|
4
|
+
WHAT IT DEFENDS AGAINST
|
|
5
|
+
-----------------------
|
|
6
|
+
`/caddis:spawn-session` hands the WORK to a fresh session. A handover fails in a known way, and
|
|
7
|
+
this module defends against it with four mechanisms:
|
|
8
8
|
|
|
9
9
|
1. capture from the repo, never from recall
|
|
10
10
|
2. a self-check whose naive answer is WRONG, so a successor that skimmed fails visibly
|
|
11
11
|
3. no stored answer key: re-derive at validation time, which checks the DOCUMENT as well
|
|
12
12
|
4. refuse to write anything that goes stale (a commit hash, a test count) as a fact
|
|
13
13
|
|
|
14
|
-
Built as one module so
|
|
15
|
-
`succession.md` §J had the trap idea and `spawn-session` was about to grow a second copy of it.
|
|
14
|
+
Built as one shared module so the mechanisms have one home and cannot drift into copies.
|
|
16
15
|
|
|
17
16
|
THE FAILURE MODE, PRECISELY
|
|
18
17
|
---------------------------
|
|
@@ -729,7 +728,7 @@ def handshake_record(repo_root: Path, spawn_id: str, event: str,
|
|
|
729
728
|
# successor's tracker starts empty — measured 2026-09-10: a parent board of 59 tasks, and the
|
|
730
729
|
# child's `TaskList` said "No tasks found". So the read-back must say how many items the child
|
|
731
730
|
# rebuilt from the file. Required on `readback` (spawn-session's parent-first flow). Optional
|
|
732
|
-
# on child-first `answers`,
|
|
731
|
+
# on child-first `answers`, which a successor can use before any task list exists.
|
|
733
732
|
if event == "readback" and revived is None:
|
|
734
733
|
return state, (f"a read-back must say how many open tasks the successor rebuilt from "
|
|
735
734
|
f"{ART}/{PARENT_STATE} into its own tracker. Ask for the number, then record "
|
|
@@ -794,8 +793,8 @@ def handshake_close(repo_root: Path, spawn_id: str) -> tuple[bool, str]:
|
|
|
794
793
|
parent's own handover. `/clear` in the parent's terminal destroys the only thing that can do
|
|
795
794
|
that, so the parent must outlive the handshake.
|
|
796
795
|
|
|
797
|
-
NOTE the distinction this makes explicit, because two
|
|
798
|
-
|
|
796
|
+
NOTE the distinction this makes explicit, because two rules look like they disagree:
|
|
797
|
+
the single-writer rule says the outgoing session STOPS WRITING once the prompt is issued (two
|
|
799
798
|
sessions committed to one repo concurrently and one's work landed inside the other's commit).
|
|
800
799
|
008 says the parent is not CLOSED until the child passes. Alive is not writing. Both hold.
|
|
801
800
|
"""
|