codedeck 0.5.2 → 0.5.3

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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/commands/doctor.js +24 -6
  3. package/dist/cli/commands/doctor.js.map +1 -1
  4. package/dist/cli/commands/profile.js +7 -2
  5. package/dist/cli/commands/profile.js.map +1 -1
  6. package/dist/cli/commands/ps.js +41 -6
  7. package/dist/cli/commands/ps.js.map +1 -1
  8. package/dist/cli/commands/setup.js +50 -22
  9. package/dist/cli/commands/setup.js.map +1 -1
  10. package/dist/cli/commands/web.js +1 -1
  11. package/dist/core/events.js.map +1 -1
  12. package/dist/core/pricing.js +51 -6
  13. package/dist/core/pricing.js.map +1 -1
  14. package/dist/daemon/daemon.js +30 -1
  15. package/dist/daemon/daemon.js.map +1 -1
  16. package/dist/drivers/antigravity/driver.js +105 -2
  17. package/dist/drivers/antigravity/driver.js.map +1 -1
  18. package/dist/drivers/antigravity/parser.js +108 -2
  19. package/dist/drivers/antigravity/parser.js.map +1 -1
  20. package/dist/drivers/claude/parser.js +3 -1
  21. package/dist/drivers/claude/parser.js.map +1 -1
  22. package/dist/drivers/opencode/parser.js +26 -1
  23. package/dist/drivers/opencode/parser.js.map +1 -1
  24. package/dist/open/launchers/opencode.js +1 -1
  25. package/dist/open/launchers/opencode.js.map +1 -1
  26. package/dist/open/runtime.js +8 -0
  27. package/dist/open/runtime.js.map +1 -1
  28. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  29. package/dist/plugin/agents/orchestrator-edit.md +11 -2
  30. package/dist/plugin/agents/orchestrator-read.md +11 -2
  31. package/dist/plugin/agents/orchestrator.md +4 -2
  32. package/dist/plugin/hooks/hooks.json +2 -0
  33. package/dist/plugin/hooks/register.tsx +251 -0
  34. package/dist/plugin/mods/agents/pane.ts +453 -0
  35. package/dist/plugin/mods/agents/parse.ts +29 -0
  36. package/dist/plugin/mods/agents/types.ts +43 -0
  37. package/dist/plugin/prompts/_partials/spec-gates.md +2 -0
  38. package/dist/plugin/prompts/roles/orchestrator-edit.md +2 -1
  39. package/dist/plugin/prompts/roles/orchestrator-read.md +2 -1
  40. package/dist/plugin/prompts/roles/orchestrator.md +2 -2
  41. package/dist/plugin/skills/tlc-spec-driven/SKILL.md +186 -0
  42. package/dist/plugin/skills/tlc-spec-driven/references/code-analysis.md +98 -0
  43. package/dist/plugin/skills/tlc-spec-driven/references/coding-principles.md +72 -0
  44. package/dist/plugin/skills/tlc-spec-driven/references/context-limits.md +31 -0
  45. package/dist/plugin/skills/tlc-spec-driven/references/design.md +199 -0
  46. package/dist/plugin/skills/tlc-spec-driven/references/discuss.md +159 -0
  47. package/dist/plugin/skills/tlc-spec-driven/references/implement.md +436 -0
  48. package/dist/plugin/skills/tlc-spec-driven/references/lessons.md +115 -0
  49. package/dist/plugin/skills/tlc-spec-driven/references/memory.md +144 -0
  50. package/dist/plugin/skills/tlc-spec-driven/references/specify.md +228 -0
  51. package/dist/plugin/skills/tlc-spec-driven/references/sub-agents.md +147 -0
  52. package/dist/plugin/skills/tlc-spec-driven/references/tasks.md +451 -0
  53. package/dist/plugin/skills/tlc-spec-driven/references/validate.md +355 -0
  54. package/dist/plugin/skills/tlc-spec-driven/scripts/check_commit.py +133 -0
  55. package/dist/plugin/skills/tlc-spec-driven/scripts/lessons.py +412 -0
  56. package/dist/plugin/skills/tlc-spec-driven/scripts/validate_spec.py +284 -0
  57. package/dist/plugin/skills/tlc-spec-driven/scripts/validate_state.py +196 -0
  58. package/dist/plugin/skills/tlc-spec-driven/scripts/validate_tasks.py +287 -0
  59. package/dist/plugin/statusline.sh +0 -1
  60. package/dist/web/canvas-page.js +82 -82
  61. package/package.json +1 -1
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_state.py - deterministic completion gate for a feature.
4
+
5
+ The skill's strongest invariant is "the Verifier is always-on, never prompted;
6
+ Execute is not done until validation.md reports PASS." That is prose the model
7
+ must remember. This turns it into a checkable pass/fail the closing step runs
8
+ automatically, so declaring a feature done without a real Verifier report fails
9
+ loudly instead of slipping through.
10
+
11
+ It does NOT merely check that validation.md exists - a report that exists but is
12
+ empty, still holds the template placeholder, or has no evidence would pass a
13
+ shallow existence check while proving nothing. This gate requires a real,
14
+ filled verdict plus at least one file:line evidence citation.
15
+
16
+ Operates only on the .specs/ markdown artifacts (stack- and tool-agnostic). No
17
+ dependencies. Run from the project root (the dir that contains .specs), or pass
18
+ --root. Meant to be invoked by the skill as the closing gate of Execute, the
19
+ same way lessons.py is invoked at distillation - not a manual step.
20
+
21
+ Usage:
22
+ python3 <skill-dir>/scripts/validate_state.py [feature]
23
+ python3 <skill-dir>/scripts/validate_state.py
24
+
25
+ Invoke from the skill directory that ships this script (not the project root).
26
+ Pass --root when cwd is not the project that contains .specs/.
27
+
28
+ Exit codes: 0 ok, 1 a completed feature is missing a real PASS report,
29
+ 2 usage error.
30
+ """
31
+
32
+ import argparse
33
+ import os
34
+ import re
35
+ import sys
36
+
37
+ def _feature_dirs(root):
38
+ base = os.path.join(root, ".specs", "features")
39
+ if not os.path.isdir(base):
40
+ return base, []
41
+ dirs = [
42
+ d for d in sorted(os.listdir(base))
43
+ if os.path.isdir(os.path.join(base, d))
44
+ ]
45
+ return base, dirs
46
+
47
+
48
+ def _verdict(text):
49
+ """Return 'pass', 'fail', 'unfilled', or None from a validation report."""
50
+ # Look at the '## Validation' heading first, then a '**Result**' line.
51
+ lines = text.splitlines()
52
+ candidates = [
53
+ ln for ln in lines
54
+ if re.search(r"^#{1,4}\s*validation\b", ln.strip(), re.IGNORECASE)
55
+ or re.search(r"\*{0,2}result\*{0,2}\s*:", ln.strip(), re.IGNORECASE)
56
+ ]
57
+ hay = " ".join(candidates) if candidates else text
58
+ has_pass = re.search(r"\bPASS\b", hay) is not None
59
+ has_fail = re.search(r"\bFAIL\b", hay) is not None
60
+ if has_pass and has_fail:
61
+ # Both present on the verdict line = unfilled template "[PASS | FAIL]".
62
+ return "unfilled"
63
+ if has_pass:
64
+ return "pass"
65
+ if has_fail:
66
+ return "fail"
67
+ return None
68
+
69
+
70
+ def _appears_complete(fdir):
71
+ """Conservative completeness heuristic for the cross-check mode.
72
+
73
+ A feature 'appears complete' if it already has a validation.md, or if it has
74
+ a tasks.md with at least one task and no unchecked '- [ ]' boxes left. When
75
+ the signal is ambiguous (no tasks.md, Tasks phase skipped), returns False so
76
+ an in-flight feature is never falsely flagged.
77
+ """
78
+ if os.path.exists(os.path.join(fdir, "validation.md")):
79
+ return True
80
+ tasks = os.path.join(fdir, "tasks.md")
81
+ if not os.path.exists(tasks):
82
+ return False
83
+ # tasks is built from the root-scoped feature directory.
84
+ with open(tasks, encoding="utf-8", errors="replace") as f: # NOSONAR
85
+ body = f.read()
86
+ if not re.search(r"^#{2,4}\s+T\d+\s*:", body, re.MULTILINE):
87
+ return False
88
+ if re.search(r"^\s*-\s*\[\s\]", body, re.MULTILINE):
89
+ return False # unchecked box remains -> still in progress
90
+ return True
91
+
92
+
93
+ def _check_feature(fdir, name):
94
+ """Return list of error strings for one feature (empty = pass)."""
95
+ errors = []
96
+ vpath = os.path.join(fdir, "validation.md")
97
+ if not os.path.exists(vpath):
98
+ errors.append(
99
+ f"{name}: no validation.md - Execute is not done until the Verifier "
100
+ f"writes it (author != verifier). Dispatch validation before marking done."
101
+ )
102
+ return errors
103
+ # vpath is built from the root-scoped feature directory.
104
+ with open(vpath, encoding="utf-8", errors="replace") as f: # NOSONAR
105
+ text = f.read()
106
+ verdict = _verdict(text)
107
+ if verdict is None:
108
+ errors.append(f"{name}: validation.md has no PASS/FAIL verdict (a prose-only report does not count)")
109
+ elif verdict == "unfilled":
110
+ errors.append(f"{name}: validation.md verdict is still the template placeholder '[PASS | FAIL]' - not filled")
111
+ elif verdict == "fail":
112
+ errors.append(f"{name}: validation.md verdict is FAIL - route the ranked gaps to fix tasks, then re-verify (feature is not done)")
113
+ if verdict == "pass" and not has_evidence(text):
114
+ errors.append(f"{name}: validation.md is PASS but cites no file:line evidence - evidence-or-zero not satisfied")
115
+ return errors
116
+
117
+
118
+ def has_evidence(text):
119
+ for raw in text.split():
120
+ token = raw.strip(chr(96) + "|()[]{}<>,.;")
121
+ path_part, separator, line = token.rpartition(":")
122
+ if not separator or not line.isdigit():
123
+ continue
124
+ _, extension_separator, extension = path_part.rpartition(".")
125
+ if extension_separator and extension.isalnum():
126
+ return True
127
+ return False
128
+
129
+
130
+ def _safe_path(candidate, root):
131
+ root = os.path.realpath(root)
132
+ candidate = os.path.realpath(os.path.abspath(candidate))
133
+ try:
134
+ inside = os.path.commonpath((root, candidate)) == root
135
+ except ValueError:
136
+ inside = False
137
+ if not inside:
138
+ raise ValueError(f"feature is outside project root: {candidate}")
139
+ return candidate
140
+
141
+
142
+ def _resolve(root, feature):
143
+ base, dirs = _feature_dirs(root)
144
+ if not os.path.isdir(base):
145
+ print(f"validate_state: no {base} directory - nothing to check.")
146
+ return []
147
+ if feature:
148
+ feature_path = os.path.realpath(os.path.abspath(feature))
149
+ if os.path.isdir(feature_path):
150
+ fdir = _safe_path(feature_path, root)
151
+ else:
152
+ if os.path.isabs(feature) or os.path.basename(feature) != feature:
153
+ raise ValueError("feature is outside project root or does not name a feature")
154
+ fdir = _safe_path(os.path.join(base, feature), root)
155
+ if not os.path.isdir(fdir):
156
+ print(f"validate_state: feature not found: {feature}", file=sys.stderr)
157
+ raise SystemExit(2)
158
+ return [(fdir, os.path.basename(fdir.rstrip("/")))]
159
+ if len(dirs) == 1:
160
+ return [(os.path.join(base, dirs[0]), dirs[0])]
161
+ if not dirs:
162
+ print("validate_state: no features under .specs/features/ - nothing to check.")
163
+ return []
164
+ # Cross-check mode: only features that appear complete.
165
+ picked = [(os.path.join(base, d), d) for d in dirs if _appears_complete(os.path.join(base, d))]
166
+ if not picked:
167
+ print("validate_state: no completed feature detected (all in progress) - nothing to gate.")
168
+ return picked
169
+
170
+
171
+ def main(argv=None):
172
+ p = argparse.ArgumentParser(prog="validate_state.py", description="Deterministic completion gate: a done feature must have a real PASS validation report.")
173
+ p.add_argument("feature", nargs="?", default=None, help="Feature dir or name (default: sole feature, else cross-check all completed)")
174
+ p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)")
175
+ args = p.parse_args(argv)
176
+ root = os.path.abspath(args.root)
177
+
178
+ try:
179
+ targets = _resolve(root, args.feature)
180
+ except ValueError as exc:
181
+ print(f"validate_state: {exc}", file=sys.stderr)
182
+ return 2
183
+ all_errors = []
184
+ for fdir, name in targets:
185
+ all_errors += _check_feature(fdir, name)
186
+
187
+ for e in all_errors:
188
+ print(f" ERROR {e}")
189
+ n = len(all_errors)
190
+ checked = ", ".join(name for _, name in targets) or "(none)"
191
+ print(f"\nvalidate_state: {n} error(s) across [{checked}]")
192
+ return 1 if n else 0
193
+
194
+
195
+ if __name__ == "__main__":
196
+ raise SystemExit(main())
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_tasks.py - deterministic pre-approval checks for a feature tasks.md.
4
+
5
+ Turns the three pre-approval checks (task granularity, diagram-vs-definition
6
+ cross-check, test co-location) into a checkable pass/fail run BEFORE tasks are
7
+ presented for approval, instead of trusting the model to build the tables by
8
+ hand. Pure standard library, zero dependencies. Operates only on the tasks.md
9
+ markdown artifact, so it is stack-agnostic and tool-agnostic.
10
+
11
+ What it checks (heuristic markdown inspection, not a full parser):
12
+ ERROR - a required section is missing
13
+ ERROR - a task is missing its `Tests` or `Gate` field
14
+ ERROR - a task depends on a task in a LATER phase (dependencies point back only)
15
+ ERROR - a dependency edge shown in the diagram has no matching `Depends on`
16
+ (and vice-versa) when both sides are parseable
17
+ WARN - a task's `Where` names multiple files (granularity smell -> split it)
18
+ WARN - a task says `Tests: none` (confirm the coverage matrix agrees)
19
+ WARN - the diagram could not be parsed confidently (cross-check skipped)
20
+
21
+ Usage:
22
+ python3 <skill-dir>/scripts/validate_tasks.py [target] [--root DIR] [--strict]
23
+
24
+ Invoke from the skill directory that ships this script (not the project root).
25
+ target Path to a tasks.md, a feature directory, or a project root.
26
+ Omitted -> auto-detect the single feature under <root>/.specs/features/.
27
+ --root Project root that contains .specs/ (default: current dir).
28
+ --strict Treat warnings as errors.
29
+
30
+ Exit codes: 0 pass, 1 errors found (or warnings under --strict), 2 usage error.
31
+ """
32
+
33
+ import argparse
34
+ import os
35
+ import re
36
+ import sys
37
+
38
+ REQUIRED_SECTIONS = ["Test Coverage Matrix", "Gate Check Commands", "Execution Plan", "Task Breakdown"]
39
+ TASK_RE = re.compile(r"^#{2,4}\s+(T\d+)\s*:", re.IGNORECASE)
40
+ EDGE_RE = re.compile(r"\bT\d+\b")
41
+
42
+
43
+ def _safe_path(candidate, root):
44
+ root = os.path.realpath(root)
45
+ candidate = os.path.realpath(os.path.abspath(candidate))
46
+ try:
47
+ inside = os.path.commonpath((root, candidate)) == root
48
+ except ValueError:
49
+ inside = False
50
+ if not inside:
51
+ raise ValueError(f"target is outside project root: {candidate}")
52
+ return candidate
53
+
54
+
55
+ def resolve_tasks(target, root):
56
+ root = os.path.realpath(root)
57
+ if target:
58
+ target_path = os.path.realpath(os.path.abspath(target))
59
+ if os.path.isfile(target_path):
60
+ return _safe_path(target_path, root)
61
+ if os.path.isdir(target_path):
62
+ target_path = _safe_path(target_path, root)
63
+ cand = os.path.join(target_path, "tasks.md")
64
+ if os.path.isfile(cand):
65
+ return cand
66
+ return _autodetect(target_path)
67
+ if os.path.isabs(target) or os.path.basename(target) != target:
68
+ raise ValueError("target is outside project root or does not name a feature")
69
+ # Not a path: treat as a feature name under <root>/.specs/features/<name>/
70
+ cand = os.path.join(root, ".specs", "features", target, "tasks.md")
71
+ if os.path.isfile(cand):
72
+ return _safe_path(cand, root)
73
+ return None
74
+ return _autodetect(root)
75
+
76
+
77
+ def _autodetect(root):
78
+ base = os.path.join(root, ".specs", "features")
79
+ if not os.path.isdir(base):
80
+ return None
81
+ features = [d for d in sorted(os.listdir(base)) if os.path.isfile(os.path.join(base, d, "tasks.md"))]
82
+ if len(features) == 1:
83
+ return os.path.join(base, features[0], "tasks.md")
84
+ if len(features) == 0:
85
+ return None
86
+ raise SystemExit(
87
+ "validate_tasks: multiple features found; pass one explicitly:\n "
88
+ + "\n ".join(os.path.join(base, f, "tasks.md") for f in features)
89
+ )
90
+
91
+
92
+ def section_present(lines, name):
93
+ return any(re.match(r"^#{1,4}\s+" + re.escape(name) + r"\b", ln.strip()) for ln in lines)
94
+
95
+
96
+ def field_value(line, name):
97
+ match = re.match(r"^\*{0,2}" + re.escape(name) + r"\*{0,2}\s*:", line, re.IGNORECASE)
98
+ return line[match.end():].strip() if match else None
99
+
100
+
101
+ def file_hints(text):
102
+ hints = []
103
+ for raw in text.split():
104
+ token = raw.strip(chr(96) + ".,;:()[]{}")
105
+ token = token.split(":", 1)[0]
106
+ stem, separator, extension = token.rpartition(".")
107
+ if separator and stem and 1 <= len(extension) <= 6 and extension.isalnum():
108
+ hints.append(token)
109
+ return hints
110
+
111
+
112
+ def parse_tasks(lines):
113
+ """Return a dict: task_id -> {'deps': set, 'tests': str|None, 'gate': str|None, 'where': str}."""
114
+ tasks = {}
115
+ current = None
116
+ for ln in lines:
117
+ m = TASK_RE.match(ln.strip())
118
+ if m:
119
+ current = m.group(1).upper()
120
+ tasks[current] = {"deps": set(), "tests": None, "gate": None, "where": ""}
121
+ continue
122
+ if current is None:
123
+ continue
124
+ stripped = ln.strip()
125
+ body = field_value(stripped, "Depends on")
126
+ if body is not None:
127
+ if "none" not in body.lower():
128
+ for e in EDGE_RE.findall(body.upper()):
129
+ tasks[current]["deps"].add(e)
130
+ value = field_value(stripped, "Where")
131
+ if value is not None:
132
+ tasks[current]["where"] = value
133
+ value = field_value(stripped, "Tests")
134
+ if value is not None:
135
+ tasks[current]["tests"] = value
136
+ value = field_value(stripped, "Gate")
137
+ if value is not None:
138
+ tasks[current]["gate"] = value
139
+ return tasks
140
+
141
+
142
+ def parse_phase_membership(lines):
143
+ """Map task_id -> phase index, read from '### Phase N' headers in the Execution Plan."""
144
+ membership = {}
145
+ phase_idx = 0
146
+ in_phase = False
147
+ for ln in lines:
148
+ pm = re.match(r"^#{2,4}\s+Phase\s+(\d+)", ln.strip(), re.IGNORECASE)
149
+ if pm:
150
+ phase_idx = int(pm.group(1))
151
+ in_phase = True
152
+ continue
153
+ if in_phase:
154
+ # Map a task to a phase ONLY when it appears as a task header (### Tn:),
155
+ # never when it is merely referenced (e.g. in a `Depends on:` line or a
156
+ # diagram arrow), which would misattribute the referenced task to this phase.
157
+ hm = TASK_RE.match(ln.strip())
158
+ if hm:
159
+ membership[hm.group(1).upper()] = phase_idx
160
+ return membership
161
+
162
+
163
+ def parse_diagram_edges(lines):
164
+ """Best-effort: parse 'Tx -> Ty' / 'Tx → Ty' arrow chains from fenced blocks.
165
+ Returns (edges:set[(src,dst)], parsed:bool)."""
166
+ edges = set()
167
+ in_fence = False
168
+ found_any_arrow = False
169
+ for ln in lines:
170
+ if ln.strip().startswith("```"):
171
+ in_fence = not in_fence
172
+ continue
173
+ if not in_fence:
174
+ continue
175
+ # normalize arrow glyphs
176
+ norm = ln.replace("→", "->").replace("──", "-").replace("-", "-")
177
+ if "->" not in norm:
178
+ continue
179
+ chain = EDGE_RE.findall(norm.upper())
180
+ # only treat as a chain if arrows connect them left-to-right
181
+ segments = [s for s in re.split(r"->", norm)]
182
+ seq = []
183
+ for seg in segments:
184
+ ids = EDGE_RE.findall(seg.upper())
185
+ seq.append(ids[-1] if ids else None)
186
+ for a, b in zip(seq, seq[1:]):
187
+ if a and b:
188
+ edges.add((a, b))
189
+ found_any_arrow = True
190
+ return edges, found_any_arrow
191
+
192
+
193
+ def check(tasks_path):
194
+ # resolve_tasks confines the canonical path to the project root.
195
+ with open(tasks_path, "r", encoding="utf-8") as f: # NOSONAR
196
+ lines = f.read().splitlines()
197
+ errors, warnings = [], []
198
+
199
+ for name in REQUIRED_SECTIONS:
200
+ if not section_present(lines, name):
201
+ errors.append(f"missing required section: ## {name}")
202
+
203
+ tasks = parse_tasks(lines)
204
+ if not tasks:
205
+ warnings.append("no tasks (### T1: ...) parsed - is this file filled in?")
206
+ return errors, warnings
207
+
208
+ # Field presence + granularity smell.
209
+ for tid, t in tasks.items():
210
+ if t["tests"] is None:
211
+ errors.append(f"{tid}: missing `Tests` field")
212
+ elif t["tests"].lower().startswith("none"):
213
+ warnings.append(f"{tid}: Tests: none - confirm the Test Coverage Matrix says 'none' for this layer")
214
+ if t["gate"] is None:
215
+ errors.append(f"{tid}: missing `Gate` field")
216
+ files = file_hints(t["where"])
217
+ if len(set(files)) > 1:
218
+ warnings.append(f"{tid}: `Where` names multiple files {sorted(set(files))} - granularity smell, consider splitting")
219
+
220
+ # Forward-phase dependency.
221
+ membership = parse_phase_membership(lines)
222
+ for tid, t in tasks.items():
223
+ p_here = membership.get(tid)
224
+ if p_here is None:
225
+ continue
226
+ for dep in t["deps"]:
227
+ p_dep = membership.get(dep)
228
+ if p_dep is not None and p_dep > p_here:
229
+ errors.append(f"{tid} (phase {p_here}) depends on {dep} (phase {p_dep}) - dependencies must point backward or within the same phase")
230
+
231
+ # Diagram vs definition cross-check (best effort).
232
+ edges, parsed = parse_diagram_edges(lines)
233
+ if not parsed:
234
+ warnings.append("diagram arrows not parsed confidently - diagram/definition cross-check skipped (verify by hand)")
235
+ else:
236
+ def intra_phase(a, b):
237
+ # Parity applies only within a phase. A backward cross-phase dependency
238
+ # is validated by the forward-phase check above and needs no diagram arrow;
239
+ # phase diagrams are drawn per phase, so cross-phase edges are out of scope here.
240
+ pa, pb = membership.get(a), membership.get(b)
241
+ if pa is None or pb is None:
242
+ return True # unknown phase -> keep best-effort parity
243
+ return pa == pb
244
+
245
+ dep_edges = set()
246
+ for tid, t in tasks.items():
247
+ for dep in t["deps"]:
248
+ dep_edges.add((dep, tid)) # arrow points dep -> task
249
+ only_in_diagram = {(a, b) for (a, b) in (edges - dep_edges) if intra_phase(a, b)}
250
+ only_in_defs = {(a, b) for (a, b) in (dep_edges - edges) if intra_phase(a, b)}
251
+ for a, b in sorted(only_in_diagram):
252
+ if a in tasks and b in tasks:
253
+ errors.append(f"diagram shows {a} -> {b} but {b} has no matching `Depends on: {a}`")
254
+ for a, b in sorted(only_in_defs):
255
+ errors.append(f"{b} declares `Depends on: {a}` but the diagram has no {a} -> {b} arrow")
256
+
257
+ return errors, warnings
258
+
259
+
260
+ def main(argv=None):
261
+ p = argparse.ArgumentParser(prog="validate_tasks.py", description="Pre-approval checks for a feature tasks.md.")
262
+ p.add_argument("target", nargs="?", default=None)
263
+ p.add_argument("--root", default=".")
264
+ p.add_argument("--strict", action="store_true")
265
+ args = p.parse_args(argv)
266
+
267
+ try:
268
+ tasks_path = resolve_tasks(args.target, args.root)
269
+ except ValueError as exc:
270
+ print(f"validate_tasks: {exc}", file=sys.stderr)
271
+ return 2
272
+ if not tasks_path:
273
+ print("validate_tasks: could not locate a tasks.md. Pass a path or run from the project root.", file=sys.stderr)
274
+ return 2
275
+
276
+ errors, warnings = check(tasks_path)
277
+ for w in warnings:
278
+ print(f" WARN {w}")
279
+ for e in errors:
280
+ print(f" ERROR {e}")
281
+ fail = errors or (warnings and args.strict)
282
+ print(f"\nvalidate_tasks: {len(errors)} error(s), {len(warnings)} warning(s) in {tasks_path}")
283
+ return 1 if fail else 0
284
+
285
+
286
+ if __name__ == "__main__":
287
+ raise SystemExit(main())
@@ -253,7 +253,6 @@ const fields = [
253
253
  contextField(),
254
254
  tokenField(),
255
255
  runUsage ? runField() : localField(),
256
- runUsage && paint(TEXT, String(Math.max(0, Math.round(runUsage.activeSessionCount))) + " agents"),
257
256
  ].filter(Boolean);
258
257
 
259
258
  writeSync(1, fields.join(paint(MUTED, " · ")));