@hallucination-studio/harness-engine 1.0.0-beta.10.9ff10d9

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.
@@ -0,0 +1,2374 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ import time
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+
12
+ MANAGED_MARKER = "<!-- harness-engine:managed -->"
13
+ DEFAULT_KNOWLEDGE_PLACEHOLDER = "- [ ] Add durable facts here as they emerge -> <destination-doc>"
14
+ DEFAULT_DEFECT_PLACEHOLDER = "None."
15
+ PLAN_TEMPLATE = """# Execution Plan: {title}
16
+
17
+ ## Goal
18
+
19
+ {goal}
20
+
21
+ ## Scope
22
+
23
+ - Define in-scope work.
24
+ - Define out-of-scope work.
25
+
26
+ ## Constraints
27
+
28
+ - Add relevant product, architecture, reliability, security, or delivery constraints.
29
+
30
+ ## Steps
31
+
32
+ 1. Add the first concrete step.
33
+ 2. Add the next concrete step.
34
+
35
+ ## Validation
36
+
37
+ - Describe how the work will be verified.
38
+
39
+ ## Quality Gate
40
+
41
+ Status: pending
42
+ Minimum score: 8.0
43
+ Average score: pending
44
+ Last scored: pending
45
+
46
+ | Dimension | Score | Notes |
47
+ | --- | ---: | --- |
48
+ | Product correctness | pending | Confirm the requested behavior is complete. |
49
+ | UX and operator clarity | pending | Confirm the user or operator experience is understandable. |
50
+ | Architecture and maintainability | pending | Confirm the implementation is clean and easy to change. |
51
+ | Reliability and observability | pending | Confirm the validation loop and failure handling are sufficient. |
52
+ | Security and data handling | pending | Confirm secrets and sensitive data are handled safely. |
53
+
54
+ ## Defects To Resolve
55
+
56
+ {defect_section}
57
+
58
+ ## Rework Required
59
+
60
+ - Pending quality score.
61
+
62
+ ## Phase Continuity
63
+
64
+ Mode: single-phase
65
+ Workstream: none
66
+ Current phase: none
67
+ Next phase: none
68
+ Continuation: none
69
+ Next action: none
70
+ Closure reason: This plan is not part of a longer workstream.
71
+ Resume notes: none
72
+
73
+ ## Durable Knowledge To Capture
74
+
75
+ {knowledge_section}
76
+
77
+ ## Completion Notes
78
+
79
+ Pending.
80
+ """
81
+
82
+ ROOT_FILES = {
83
+ "AGENTS.md": """{marker}
84
+ # AGENTS
85
+
86
+ Read this file first, then follow the linked docs.
87
+
88
+ ## Routing
89
+
90
+ - Read `ARCHITECTURE.md` before changing boundaries, data flow, or integrations.
91
+ - Read `docs/PLANS.md` before starting multi-step execution work.
92
+ - Read `docs/exec-plans/workstreams.md` before resuming interrupted feature, refactor, reliability, or cleanup work.
93
+ - Read `docs/exec-plans/active/` before resuming in-flight work, and create a plan there for new multi-step work.
94
+ - Read `docs/QUALITY_SCORE.md` before evaluating tradeoffs or readiness.
95
+ - Read `docs/RELIABILITY.md` for runtime validation and failure handling.
96
+ - Read `docs/SECURITY.md` before touching auth, secrets, or sensitive data.
97
+ - Read `docs/FRONTEND.md` for UI or terminal interface changes.
98
+ - Read the matching file in `docs/sops/` before architecture changes, UI validation, observability work, evidence-first evals, or knowledge capture.
99
+
100
+ ## Issue Workflows
101
+
102
+ For any user-reported issue, classify the domain first, read the listed files, then reproduce,
103
+ fix, and validate with evidence before judging the result.
104
+
105
+ | Domain | Read First | Required Evidence |
106
+ | --- | --- | --- |
107
+ | Product contract or acceptance drift | `docs/PRODUCT_SENSE.md`, `docs/product-specs/`, `docs/sops/evidence-first-eval-loop.md` | Product assertions, acceptance checks, or documented limitation |
108
+ | Frontend, UI, layout, interaction, responsive, canvas, visual state, or design fidelity | `docs/FRONTEND.md`, `docs/DESIGN.md`, `docs/sops/evidence-first-eval-loop.md` | Browser or local-runtime evidence across relevant workflows and viewports |
109
+ | Backend, API, runtime behavior, background jobs, or integrations | `ARCHITECTURE.md`, `docs/RELIABILITY.md`, `docs/sops/local-observability-feedback-loop.md` | Narrow reproduction, tests or API smoke checks, logs, and failure-mode evidence |
110
+ | Architecture boundaries, layering, data flow, or dependency direction | `ARCHITECTURE.md`, `docs/PLANS.md`, `docs/sops/layered-domain-architecture-setup.md` | Boundary map, tradeoff notes, migration or compatibility plan, and validation path |
111
+ | Data, state, migrations, cache, queues, or file formats | `ARCHITECTURE.md`, `docs/RELIABILITY.md`, `docs/SECURITY.md` | Fixtures or migration checks, rollback/compatibility evidence, and data-loss risk notes |
112
+ | Security, privacy, auth, authorization, secrets, or sensitive data | `docs/SECURITY.md`, `ARCHITECTURE.md` | Threat check, sensitive-data path, permission test, and secret-handling evidence |
113
+ | Performance, capacity, timeout, resource use, or availability | `docs/RELIABILITY.md`, `ARCHITECTURE.md`, `docs/sops/local-observability-feedback-loop.md` | Baseline measurement, repeatable benchmark or smoke check, and before/after evidence |
114
+
115
+ For each issue:
116
+
117
+ - Inspect the relevant code path, runtime path, and user/operator workflow.
118
+ - If a code change is needed and no active plan exists, create one with `plan-start`.
119
+ - Convert the issue into assertions, tests, smoke checks, or a regression case before changing code.
120
+ - Log confirmed defects or missing evidence with `defect-log`; unresolved defects must block `quality-score`, `plan-close`, and handoff.
121
+ - Verify the fix against the same workflow and evidence type before claiming it is resolved.
122
+
123
+ ## Repository Focus
124
+
125
+ - Project: {project_name}
126
+ - Domain: {product_domain}
127
+ - Primary outcome: {project_summary}
128
+ - Main users: {primary_users}
129
+
130
+ ## Operating Rules
131
+
132
+ - Keep durable decisions in repo docs, not only in chat.
133
+ - Keep active plans in `docs/exec-plans/active/`.
134
+ - Keep resumable feature, refactor, reliability, and cleanup work in `docs/exec-plans/workstreams.md`.
135
+ - Move completed plans to `docs/exec-plans/completed/`.
136
+ - Update plans during the work, not only at the end.
137
+ - Score completed work with `quality-score` before closing an execution plan.
138
+ - If `quality-score` fails, treat `## Rework Required` as the next implementation input and do not close the plan.
139
+ - Encode durable facts learned during execution into permanent docs before closing the task.
140
+ - Before handoff, run the local harness check: `python3 .codex/skills/harness-engine/scripts/manage_harness.py check --repo .`.
141
+ - Keep generated artifacts in `docs/generated/`.
142
+ - Keep external references in `docs/references/`.
143
+ """,
144
+ "ARCHITECTURE.md": """{marker}
145
+ # Architecture
146
+
147
+ ## System Summary
148
+
149
+ {project_summary}
150
+
151
+ ## Domain Boundaries
152
+
153
+ - Product domain: {product_domain}
154
+ - Primary users: {primary_users}
155
+ - Deployment targets: {deployment_targets}
156
+
157
+ ## Repository Shape
158
+
159
+ - Detected languages: {languages}
160
+ - Detected package managers: {package_managers}
161
+ - Detected frameworks: {frameworks}
162
+
163
+ ## Reliability Architecture
164
+
165
+ {reliability_targets}
166
+
167
+ ## Security Architecture
168
+
169
+ {security_constraints}
170
+
171
+ ## Open Questions
172
+
173
+ - Document major runtime boundaries, shared libraries, and integration seams here as the codebase grows.
174
+ """,
175
+ }
176
+
177
+ DOC_FILES = {
178
+ "docs/DESIGN.md": """{marker}
179
+ # Design
180
+
181
+ ## Product Experience Bar
182
+
183
+ {frontend_stack_notes}
184
+
185
+ ## Review Heuristics
186
+
187
+ - Prefer intentional interaction patterns over generic defaults.
188
+ - Keep visual and UX rationale durable in `docs/design-docs/`.
189
+ - Validate meaningful UI work in a real browser before closing it out.
190
+ """,
191
+ "docs/FRONTEND.md": """{marker}
192
+ # Frontend
193
+
194
+ ## Scope
195
+
196
+ {frontend_scope}
197
+
198
+ ## Stack Notes
199
+
200
+ {frontend_stack_notes}
201
+
202
+ ## Validation Loop
203
+
204
+ {frontend_validation_loop}
205
+
206
+ ## Evidence For Meaningful UI Work
207
+
208
+ - Capture desktop and mobile evidence for significant UI changes.
209
+ - Assert primary text, controls, selected state, loading state, empty state, error state, and primary interactions from the DOM or accessibility tree.
210
+ - Define and verify layout invariants for the changed surface, including readable content, non-overlapping controls, usable primary work area, stable fixed-format elements, and reachable actions.
211
+ - For responsive UI, verify that navigation, side panels, inspectors, toolbars, and secondary panes preserve the primary task area at intended breakpoints.
212
+ - For canvas, WebGL, or game UIs, add pixel or scene-state checks so a blank render cannot pass.
213
+ - Record browser limitations and fallback checks instead of claiming full UX validation when browser evidence is unavailable.
214
+ """,
215
+ "docs/PLANS.md": """{marker}
216
+ # Plans
217
+
218
+ ## Plan Lifecycle
219
+
220
+ - Put active execution plans in `docs/exec-plans/active/`.
221
+ - Move completed plans to `docs/exec-plans/completed/`.
222
+ - Track resumable multi-plan workstreams in `docs/exec-plans/workstreams.md`.
223
+ - Record cross-cutting follow-up work in `docs/exec-plans/tech-debt-tracker.md`.
224
+
225
+ ## Authoring Rules
226
+
227
+ - Keep plans concrete, testable, and scoped.
228
+ - Update plans during the work, not after the fact.
229
+ - Link to specs, decisions, and validation artifacts when they exist.
230
+ - Include a section for durable knowledge that must be written back into permanent docs.
231
+ - Include phase continuity when a plan is part of a multi-phase feature, refactor, reliability, or cleanup effort.
232
+ - Do not treat plans as the final home for product, architecture, or policy knowledge.
233
+ """,
234
+ "docs/PRODUCT_SENSE.md": """{marker}
235
+ # Product Sense
236
+
237
+ ## Product Summary
238
+
239
+ {project_summary}
240
+
241
+ ## Users
242
+
243
+ {primary_users}
244
+
245
+ ## Decision Rules
246
+
247
+ - Optimize for the main user outcome before edge polish.
248
+ - Make tradeoffs explicit when speed, quality, and scope conflict.
249
+ - Capture durable product decisions in `docs/product-specs/`.
250
+ """,
251
+ "docs/QUALITY_SCORE.md": """{marker}
252
+ # Quality Score
253
+
254
+ ## Priority Areas
255
+
256
+ {quality_focus}
257
+
258
+ ## Scoring Dimensions
259
+
260
+ - Product correctness
261
+ - UX and operator clarity
262
+ - Architecture and maintainability
263
+ - Reliability and observability
264
+ - Security and data handling
265
+
266
+ ## Evidence Requirements
267
+
268
+ - Product correctness scores must cite product contract checks, tests, browser assertions, or documented limitations.
269
+ - UX scores for frontend work must cite browser evidence such as screenshots, DOM/accessibility snapshots, or responsive viewport checks.
270
+ - Backend and runtime scores must cite narrow reproductions, tests, API smoke checks, logs, or integration evidence.
271
+ - Architecture scores must cite boundary, dependency, data-flow, migration, or compatibility evidence.
272
+ - Data and state scores must cite fixtures, migration checks, rollback checks, or data-loss risk analysis.
273
+ - Security scores must cite threat checks, permission tests, sensitive-data path review, or secret-handling evidence.
274
+ - Performance and reliability scores must cite baseline measurements, repeatable checks, failure-mode tests, or before/after evidence.
275
+ - Reliability scores must cite repeatable commands, smoke checks, logs, traces, or failure-mode tests.
276
+ - Every quality-score dimension requires a concrete evidence note; do not leave score notes empty.
277
+ - Open defects must be logged with `defect-log`; do not hide known failures inside a high numeric score.
278
+ - Treat LLM or human judgment as a summary over evidence, not as the only eval signal.
279
+
280
+ ## Usage
281
+
282
+ - Score changes by affected domain and layer.
283
+ - Read `AGENTS.md` Issue Workflows and `docs/sops/evidence-first-eval-loop.md` before closing work that could regress product behavior, frontend layout, backend behavior, architecture boundaries, data safety, security, performance, or bug detection.
284
+ - Document recurring weak spots and improvement themes here.
285
+ """,
286
+ "docs/RELIABILITY.md": """{marker}
287
+ # Reliability
288
+
289
+ ## Reliability Targets
290
+
291
+ {reliability_targets}
292
+
293
+ ## Runtime Validation
294
+
295
+ - Define the smallest useful local validation loop.
296
+ - Document required health checks, logs, and dashboards.
297
+ - Capture recurring incidents or near misses in repo docs.
298
+ """,
299
+ "docs/SECURITY.md": """{marker}
300
+ # Security
301
+
302
+ ## Security Constraints
303
+
304
+ {security_constraints}
305
+
306
+ ## Review Rules
307
+
308
+ - Review auth, authorization, secrets, and sensitive data changes explicitly.
309
+ - Prefer least privilege and traceable configuration.
310
+ - Record security-sensitive assumptions in durable docs.
311
+ """,
312
+ "docs/design-docs/index.md": """{marker}
313
+ # Design Docs Index
314
+
315
+ - Add one document per durable design decision.
316
+ - Link active design decisions from plans and specs.
317
+ """,
318
+ "docs/design-docs/core-beliefs.md": """{marker}
319
+ # Core Beliefs
320
+
321
+ - Keep the repository as the system of record.
322
+ - Prefer explicit policies over implied team memory.
323
+ - Prefer repeatable checks over remembered rules.
324
+ """,
325
+ "docs/exec-plans/tech-debt-tracker.md": """{marker}
326
+ # Tech Debt Tracker
327
+
328
+ Record follow-up work that should survive beyond a single execution plan.
329
+ """,
330
+ "docs/exec-plans/workstreams.md": """{marker}
331
+ # Workstreams
332
+
333
+ Use this ledger to recover interrupted feature, refactor, reliability, security, frontend, and cleanup work.
334
+
335
+ ## Index
336
+
337
+ | ID | Status | Current Plan | Last Completed Plan | Next Action | Last Updated |
338
+ | --- | --- | --- | --- | --- | --- |
339
+
340
+ ## Operating Rules
341
+
342
+ - Add a workstream when work spans multiple execution plans or may be resumed by another agent.
343
+ - Keep `Current Plan` pointed at the active plan when one exists.
344
+ - Keep `Last Completed Plan` pointed at the latest completed plan after `plan-close`.
345
+ - Keep `Next Action` concrete enough that another agent can resume without chat history.
346
+ - If a workstream is paused, record the restart condition in `Next Action`.
347
+ """,
348
+ "docs/exec-plans/active/README.md": """{marker}
349
+ # Active Execution Plans
350
+
351
+ Create one markdown file per in-flight multi-step task.
352
+
353
+ Suggested filename:
354
+
355
+ `YYYY-MM-DD-short-task-name.md`
356
+
357
+ Minimum contents:
358
+
359
+ - goal
360
+ - scope
361
+ - constraints
362
+ - steps
363
+ - validation
364
+ - quality gate
365
+ - phase continuity
366
+ - durable knowledge to capture
367
+ """,
368
+ "docs/exec-plans/active/_template.md": """{marker}
369
+ # Execution Plan: <title>
370
+
371
+ ## Goal
372
+
373
+ Describe the intended outcome.
374
+
375
+ ## Scope
376
+
377
+ Describe what is included and excluded.
378
+
379
+ ## Constraints
380
+
381
+ List product, architecture, reliability, security, or delivery constraints.
382
+
383
+ ## Steps
384
+
385
+ 1. Add the first concrete step.
386
+ 2. Add the next step.
387
+
388
+ ## Validation
389
+
390
+ - Describe how the work will be verified.
391
+
392
+ ## Quality Gate
393
+
394
+ Status: pending
395
+ Minimum score: 8.0
396
+ Average score: pending
397
+ Last scored: pending
398
+
399
+ | Dimension | Score | Notes |
400
+ | --- | ---: | --- |
401
+ | Product correctness | pending | Confirm the requested behavior is complete. |
402
+ | UX and operator clarity | pending | Confirm the user or operator experience is understandable. |
403
+ | Architecture and maintainability | pending | Confirm the implementation is clean and easy to change. |
404
+ | Reliability and observability | pending | Confirm the validation loop and failure handling is sufficient. |
405
+ | Security and data handling | pending | Confirm secrets and sensitive data are handled safely. |
406
+
407
+ ## Rework Required
408
+
409
+ - Pending quality score.
410
+
411
+ ## Phase Continuity
412
+
413
+ Mode: single-phase
414
+ Workstream: none
415
+ Current phase: none
416
+ Next phase: none
417
+ Continuation: none
418
+ Next action: none
419
+ Closure reason: This plan is not part of a longer workstream.
420
+ Resume notes: none
421
+
422
+ ## Durable Knowledge To Capture
423
+
424
+ - List facts that must be written back into permanent docs before completion.
425
+
426
+ ## Completion Notes
427
+
428
+ Summarize outcomes, follow-ups, and doc updates.
429
+ """,
430
+ "docs/exec-plans/completed/README.md": """{marker}
431
+ # Completed Execution Plans
432
+
433
+ Move finished plans here after:
434
+
435
+ 1. validation is complete
436
+ 2. the quality gate has passed
437
+ 3. phase continuity has been recorded for multi-phase work
438
+ 4. permanent docs have been updated
439
+ 5. any remaining follow-ups are recorded in workstreams, tech debt, or new plans
440
+ """,
441
+ "docs/generated/db-schema.md": """{marker}
442
+ # Generated DB Schema
443
+
444
+ Place generated database or storage schema snapshots here when relevant.
445
+ """,
446
+ "docs/product-specs/index.md": """{marker}
447
+ # Product Specs Index
448
+
449
+ - Add one durable product spec per important workflow or product area.
450
+ - Link the active plan that created or changed each spec when useful.
451
+ """,
452
+ "docs/product-specs/new-user-onboarding.md": """{marker}
453
+ # New User Onboarding
454
+
455
+ ## Outcome
456
+
457
+ Describe the desired first successful experience for a new user of {project_name}.
458
+
459
+ ## Open Questions
460
+
461
+ - What must a new user understand before reaching value?
462
+ - Which steps are fragile or confusing today?
463
+ """,
464
+ "docs/references/design-system-reference-llms.txt": "Add model-friendly design system notes or links here.\n",
465
+ "docs/references/nixpacks-llms.txt": "Add model-friendly deployment or buildpack notes here.\n",
466
+ "docs/references/uv-llms.txt": "Add model-friendly Python tooling notes here.\n",
467
+ "docs/sops/layered-domain-architecture-setup.md": """{marker}
468
+ # SOP: Layered Domain Architecture Setup
469
+
470
+ 1. Identify user-facing domains and bounded contexts.
471
+ 2. Map code ownership and integration seams.
472
+ 3. Record allowed dependency direction between layers.
473
+ 4. Capture the result in `ARCHITECTURE.md` and the relevant design docs.
474
+ """,
475
+ "docs/sops/encode-unseen-knowledge.md": """{marker}
476
+ # SOP: Encode Unseen Knowledge
477
+
478
+ 1. Notice repeated chat-only facts or tribal knowledge.
479
+ 2. Decide the right durable home inside `docs/`.
480
+ 3. Write the fact in concise, retrievable language.
481
+ 4. Link it from the nearest routing doc if it will be reused often.
482
+ """,
483
+ "docs/sops/local-observability-feedback-loop.md": """{marker}
484
+ # SOP: Local Observability Feedback Loop
485
+
486
+ 1. Run the narrowest local reproduction of the issue.
487
+ 2. Capture logs, metrics, traces, or screenshots.
488
+ 3. Tighten the validation loop until failures are easy to observe.
489
+ 4. Record the durable validation path in `docs/RELIABILITY.md`.
490
+ """,
491
+ "docs/sops/chrome-devtools-ui-validation-loop.md": """{marker}
492
+ # SOP: Chrome DevTools UI Validation Loop
493
+
494
+ 1. Open the relevant route in a browser.
495
+ 2. Check layout, interaction, loading, error, and empty states.
496
+ 3. Verify responsive behavior for the intended breakpoints.
497
+ 4. Write reusable findings back to `docs/FRONTEND.md` or `docs/design-docs/`.
498
+ """,
499
+ "docs/sops/evidence-first-eval-loop.md": """{marker}
500
+ # SOP: Evidence-First Eval Loop
501
+
502
+ 1. Convert product requirements into explicit product contract checks before scoring.
503
+ 2. Run deterministic validation first: tests, API smoke checks, CLI checks, browser actions, and state assertions.
504
+ 3. Read the Issue Workflows in `AGENTS.md` and the domain docs named there before judging or fixing.
505
+ 4. For frontend work, capture browser evidence: screenshots, DOM/accessibility snapshots, responsive checks, and layout invariants.
506
+ 5. For backend, architecture, data, security, and performance work, capture the domain evidence named in `AGENTS.md`.
507
+ 6. Log every discovered bug or evidence gap with `defect-log` before running `quality-score`.
508
+ 7. Resolve defects only after fixes have passing evidence, then rerun validation and `quality-score`.
509
+ 8. Report per-case results, failed assertions, artifact paths, and recommended next actions to the user.
510
+ """,
511
+ }
512
+
513
+ QUESTION_CATALOG = [
514
+ {
515
+ "id": "project_summary",
516
+ "prompt": "What is the main user or business outcome this repository exists to deliver?",
517
+ "reason": "Needed for AGENTS, ARCHITECTURE, and product docs.",
518
+ },
519
+ {
520
+ "id": "primary_users",
521
+ "prompt": "Who are the primary users or operators of this repository?",
522
+ "reason": "Needed to make product and quality tradeoffs concrete.",
523
+ },
524
+ {
525
+ "id": "deployment_targets",
526
+ "prompt": "Where does this system run or get deployed?",
527
+ "reason": "Needed for architecture and reliability guidance.",
528
+ },
529
+ {
530
+ "id": "product_domain",
531
+ "prompt": "Which product domain best describes this repository?",
532
+ "reason": "Needed for quality scoring and policy language.",
533
+ },
534
+ {
535
+ "id": "reliability_targets",
536
+ "prompt": "Which uptime, recovery, or runtime validation expectations matter most?",
537
+ "reason": "Needed for reliability docs and validation loops.",
538
+ },
539
+ {
540
+ "id": "security_constraints",
541
+ "prompt": "Which security, compliance, auth, or sensitive-data constraints matter here?",
542
+ "reason": "Needed for security review guidance.",
543
+ },
544
+ {
545
+ "id": "frontend_stack_notes",
546
+ "prompt": "If there is a frontend, what experience bar, platforms, or UX constraints should the docs enforce?",
547
+ "reason": "Needed for design and frontend policies.",
548
+ },
549
+ {
550
+ "id": "quality_focus",
551
+ "prompt": "Which product areas or architectural layers deserve the strictest quality scoring?",
552
+ "reason": "Needed for QUALITY_SCORE.md.",
553
+ },
554
+ ]
555
+
556
+ QUALITY_DIMENSIONS = [
557
+ ("product_correctness", "Product correctness"),
558
+ ("ux_operator_clarity", "UX and operator clarity"),
559
+ ("architecture_maintainability", "Architecture and maintainability"),
560
+ ("reliability_observability", "Reliability and observability"),
561
+ ("security_data_handling", "Security and data handling"),
562
+ ]
563
+ QUALITY_NOTE_ARGS = {
564
+ "product_correctness": "product-note",
565
+ "ux_operator_clarity": "ux-note",
566
+ "architecture_maintainability": "architecture-note",
567
+ "reliability_observability": "reliability-note",
568
+ "security_data_handling": "security-note",
569
+ }
570
+
571
+
572
+ def detect_languages(files):
573
+ ext_map = {}
574
+ for file_path in files:
575
+ suffix = Path(file_path).suffix.lower()
576
+ if suffix:
577
+ ext_map[suffix] = ext_map.get(suffix, 0) + 1
578
+ mapping = {
579
+ ".js": "JavaScript",
580
+ ".jsx": "JavaScript",
581
+ ".ts": "TypeScript",
582
+ ".tsx": "TypeScript",
583
+ ".sh": "Shell",
584
+ ".bash": "Shell",
585
+ ".zsh": "Shell",
586
+ ".py": "Python",
587
+ ".rb": "Ruby",
588
+ ".go": "Go",
589
+ ".rs": "Rust",
590
+ ".java": "Java",
591
+ ".kt": "Kotlin",
592
+ ".swift": "Swift",
593
+ }
594
+ languages = []
595
+ for ext, language in mapping.items():
596
+ if ext in ext_map and language not in languages:
597
+ languages.append(language)
598
+ return languages
599
+
600
+
601
+ def read_json_if_exists(path):
602
+ if not path.exists():
603
+ return None
604
+ try:
605
+ return json.loads(path.read_text())
606
+ except json.JSONDecodeError:
607
+ return None
608
+
609
+
610
+ def detect_frameworks(repo):
611
+ frameworks = []
612
+ package_json = read_json_if_exists(repo / "package.json")
613
+ if package_json:
614
+ deps = {}
615
+ deps.update(package_json.get("dependencies", {}))
616
+ deps.update(package_json.get("devDependencies", {}))
617
+ dep_names = set(deps.keys())
618
+ known = {
619
+ "next": "Next.js",
620
+ "react": "React",
621
+ "vue": "Vue",
622
+ "svelte": "Svelte",
623
+ "vite": "Vite",
624
+ "express": "Express",
625
+ "nestjs": "NestJS",
626
+ }
627
+ for key, label in known.items():
628
+ if key in dep_names and label not in frameworks:
629
+ frameworks.append(label)
630
+ if (repo / "pyproject.toml").exists():
631
+ text = (repo / "pyproject.toml").read_text()
632
+ if "fastapi" in text.lower():
633
+ frameworks.append("FastAPI")
634
+ if "django" in text.lower():
635
+ frameworks.append("Django")
636
+ if "flask" in text.lower():
637
+ frameworks.append("Flask")
638
+ return frameworks
639
+
640
+
641
+ def detect_package_managers(repo):
642
+ package_managers = []
643
+ markers = {
644
+ "package-lock.json": "npm",
645
+ "pnpm-lock.yaml": "pnpm",
646
+ "yarn.lock": "yarn",
647
+ "bun.lockb": "bun",
648
+ "pyproject.toml": "uv/pip",
649
+ "requirements.txt": "pip",
650
+ "go.mod": "go",
651
+ "Cargo.toml": "cargo",
652
+ }
653
+ for marker, label in markers.items():
654
+ if (repo / marker).exists():
655
+ package_managers.append(label)
656
+ return package_managers
657
+
658
+
659
+ def list_repo_files(repo):
660
+ ignored = {".git", ".codex", "node_modules", ".next", "dist", "build", "__pycache__"}
661
+ results = []
662
+ for root, dirs, files in os.walk(repo):
663
+ dirs[:] = [d for d in dirs if d not in ignored]
664
+ for filename in files:
665
+ path = Path(root, filename)
666
+ results.append(str(path.relative_to(repo)))
667
+ return sorted(results)
668
+
669
+
670
+ def detect_existing_managed_files(repo):
671
+ managed = []
672
+ for relative_path in list(ROOT_FILES.keys()) + list(DOC_FILES.keys()):
673
+ path = repo / relative_path
674
+ if path.exists():
675
+ try:
676
+ if is_managed_text(path.read_text()):
677
+ managed.append(relative_path)
678
+ except UnicodeDecodeError:
679
+ continue
680
+ return managed
681
+
682
+
683
+ def make_default_answers(analysis):
684
+ repo_name = analysis["project_name"]
685
+ frameworks = ", ".join(analysis["frameworks"]) or "Unknown"
686
+ has_frontend = analysis["has_frontend"]
687
+ frontend_scope = (
688
+ "User-facing or operator-facing frontend work is expected."
689
+ if has_frontend
690
+ else "No clear frontend surface was detected yet. Update this if a UI emerges."
691
+ )
692
+ frontend_validation_loop = (
693
+ "- Run local UI changes in a browser.\n"
694
+ "- Check desktop and mobile layouts when relevant.\n"
695
+ "- Verify key flows, empty states, and failure states.\n"
696
+ "- Record reusable UI findings in `docs/design-docs/`."
697
+ if has_frontend
698
+ else "- Validate interface changes in the relevant local runtime.\n"
699
+ "- Verify key flows, empty states, failure states, and cleanup behavior where applicable.\n"
700
+ "- Record reusable interface findings in `docs/design-docs/`."
701
+ )
702
+ defaults = {
703
+ "project_name": repo_name,
704
+ "project_summary": f"Summarize the main outcome that {repo_name} should deliver.",
705
+ "primary_users": "Describe the primary users, operators, or internal teams.",
706
+ "deployment_targets": "Describe the main runtime or deployment targets.",
707
+ "product_domain": "Describe the product domain in one line.",
708
+ "reliability_targets": "Describe uptime, failure tolerance, recovery expectations, and required validation loops.",
709
+ "security_constraints": "Describe auth, secrets, compliance, sensitive data, and review constraints.",
710
+ "frontend_stack_notes": (
711
+ f"Detected frameworks: {frameworks}. Describe UX expectations, supported environments, and review rules."
712
+ if has_frontend
713
+ else "No frontend detected. Replace this if the repo includes UI work."
714
+ ),
715
+ "quality_focus": "List the product areas and architectural layers that deserve the strictest quality bar.",
716
+ "frontend_scope": frontend_scope,
717
+ "frontend_validation_loop": frontend_validation_loop,
718
+ }
719
+ return defaults
720
+
721
+
722
+ def fill_template(template, answers, analysis):
723
+ merged = {}
724
+ merged.update(make_default_answers(analysis))
725
+ merged.update(answers)
726
+ merged.update(
727
+ {
728
+ "marker": MANAGED_MARKER,
729
+ "languages": ", ".join(analysis["languages"]) or "Unknown",
730
+ "package_managers": ", ".join(analysis["package_managers"]) or "Unknown",
731
+ "frameworks": ", ".join(analysis["frameworks"]) or "Unknown",
732
+ }
733
+ )
734
+ return template.format(**merged)
735
+
736
+
737
+ def ensure_parent(path):
738
+ path.parent.mkdir(parents=True, exist_ok=True)
739
+
740
+
741
+ def is_managed_text(text):
742
+ return text.startswith(MANAGED_MARKER)
743
+
744
+
745
+ def slugify(value):
746
+ normalized = re.sub(r"[^a-z0-9]+", "-", value.strip().lower()).strip("-")
747
+ return normalized or "task"
748
+
749
+
750
+ def find_section(lines, heading):
751
+ target = heading.strip().lower()
752
+ for index, line in enumerate(lines):
753
+ if line.strip().lower() == target:
754
+ return index
755
+ return None
756
+
757
+
758
+ def extract_knowledge_items(text):
759
+ lines = text.splitlines()
760
+ section_index = find_section(lines, "## Durable Knowledge To Capture")
761
+ if section_index is None:
762
+ return []
763
+ items = []
764
+ for line in lines[section_index + 1 :]:
765
+ if line.startswith("## "):
766
+ break
767
+ stripped = line.strip()
768
+ if stripped.startswith("- ["):
769
+ items.append(stripped)
770
+ return items
771
+
772
+
773
+ def extract_defect_items(text):
774
+ lines = text.splitlines()
775
+ section_index = find_section(lines, "## Defects To Resolve")
776
+ if section_index is None:
777
+ return []
778
+ items = []
779
+ for line in lines[section_index + 1 :]:
780
+ if line.startswith("## "):
781
+ break
782
+ stripped = line.strip()
783
+ if stripped.startswith("- ["):
784
+ items.append(stripped)
785
+ return items
786
+
787
+
788
+ def knowledge_id_for(fact, destination):
789
+ digest = hashlib.sha1(f"{clean_destination_text(destination)}\0{clean_fact_text(fact)}".encode()).hexdigest()
790
+ return f"hk-{digest[:10]}"
791
+
792
+
793
+ def defect_id_for(summary):
794
+ digest = hashlib.sha1(clean_fact_text(summary).encode()).hexdigest()
795
+ return f"bug-{digest[:10]}"
796
+
797
+
798
+ def parse_knowledge_item(item):
799
+ match = re.match(
800
+ r"- \[(?P<status>[ xX])\]\s+"
801
+ r"(?:\[(?:id|kid):(?P<id>[A-Za-z0-9_.:-]+)\]\s+)?"
802
+ r"(?P<fact>.*?)\s+->\s+"
803
+ r"(?P<destination>[^|]+?)"
804
+ r"(?:\s+\|\s+evidence:\s+(?P<evidence>.+))?$",
805
+ item.strip(),
806
+ )
807
+ if not match:
808
+ return None
809
+ return {
810
+ "status": "closed" if match.group("status").lower() == "x" else "open",
811
+ "id": match.group("id"),
812
+ "fact": clean_fact_text(match.group("fact")),
813
+ "destination": clean_destination_text(match.group("destination")),
814
+ "evidence": clean_fact_text(match.group("evidence")) if match.group("evidence") else None,
815
+ "raw": item,
816
+ }
817
+
818
+
819
+ def parse_defect_item(item):
820
+ match = re.match(
821
+ r"- \[(?P<status>[ xX])\]\s+"
822
+ r"(?:\[(?:id|bug):(?P<id>[A-Za-z0-9_.:-]+)\]\s+)?"
823
+ r"\[(?P<severity>P[0-3])\]\s+"
824
+ r"(?P<summary>.*?)"
825
+ r"(?:\s+\|\s+evidence:\s+(?P<evidence>.*?))?"
826
+ r"(?:\s+\|\s+fix:\s+(?P<fix>.+))?$",
827
+ item.strip(),
828
+ )
829
+ if not match:
830
+ return None
831
+ return {
832
+ "status": "closed" if match.group("status").lower() == "x" else "open",
833
+ "id": match.group("id"),
834
+ "severity": match.group("severity"),
835
+ "summary": clean_fact_text(match.group("summary")),
836
+ "evidence": clean_fact_text(match.group("evidence")) if match.group("evidence") else None,
837
+ "fix": clean_fact_text(match.group("fix")) if match.group("fix") else None,
838
+ "raw": item,
839
+ }
840
+
841
+
842
+ def clean_fact_text(value):
843
+ cleaned = value.strip()
844
+ cleaned = cleaned.replace("`", "")
845
+ cleaned = re.sub(r"\s+", " ", cleaned)
846
+ return cleaned.strip()
847
+
848
+
849
+ def clean_destination_text(value):
850
+ return value.strip().strip("`")
851
+
852
+
853
+ def replace_completion_notes(text, summary):
854
+ lines = text.splitlines()
855
+ section_index = find_section(lines, "## Completion Notes")
856
+ if section_index is None:
857
+ return text.rstrip() + "\n\n## Completion Notes\n\n" + summary + "\n"
858
+ end_index = len(lines)
859
+ for index in range(section_index + 1, len(lines)):
860
+ if lines[index].startswith("## "):
861
+ end_index = index
862
+ break
863
+ new_lines = lines[: section_index + 1] + ["", summary] + lines[end_index:]
864
+ return "\n".join(new_lines).rstrip() + "\n"
865
+
866
+
867
+ def replace_section(text, heading, body):
868
+ lines = text.splitlines()
869
+ section_index = find_section(lines, f"## {heading}")
870
+ if section_index is None:
871
+ return text.rstrip() + f"\n\n## {heading}\n\n{body.rstrip()}\n"
872
+ end_index = len(lines)
873
+ for index in range(section_index + 1, len(lines)):
874
+ if lines[index].startswith("## "):
875
+ end_index = index
876
+ break
877
+ new_lines = lines[: section_index + 1] + ["", body.rstrip()] + lines[end_index:]
878
+ return "\n".join(new_lines).rstrip() + "\n"
879
+
880
+
881
+ def quality_gate_for_plan(text):
882
+ lines = text.splitlines()
883
+ section_index = find_section(lines, "## Quality Gate")
884
+ if section_index is None:
885
+ return {"status": "missing", "minimum": None, "average": None, "scores": {}}
886
+ section_lines = []
887
+ for line in lines[section_index + 1 :]:
888
+ if line.startswith("## "):
889
+ break
890
+ section_lines.append(line)
891
+ section_text = "\n".join(section_lines)
892
+ status_match = re.search(r"^Status:\s*(?P<status>\w+)", section_text, flags=re.MULTILINE)
893
+ minimum_match = re.search(r"^Minimum score:\s*(?P<score>[0-9]+(?:\.[0-9]+)?)", section_text, flags=re.MULTILINE)
894
+ average_match = re.search(r"^Average score:\s*(?P<score>[0-9]+(?:\.[0-9]+)?)", section_text, flags=re.MULTILINE)
895
+ scores = {}
896
+ for _, label in QUALITY_DIMENSIONS:
897
+ row_match = re.search(
898
+ rf"^\|\s*{re.escape(label)}\s*\|\s*(?P<score>[0-9]+(?:\.[0-9]+)?)\s*\|",
899
+ section_text,
900
+ flags=re.MULTILINE,
901
+ )
902
+ if row_match:
903
+ scores[label] = float(row_match.group("score"))
904
+ return {
905
+ "status": status_match.group("status").lower() if status_match else "missing",
906
+ "minimum": float(minimum_match.group("score")) if minimum_match else None,
907
+ "average": float(average_match.group("score")) if average_match else None,
908
+ "scores": scores,
909
+ }
910
+
911
+
912
+ def section_key_values(text, heading):
913
+ lines = text.splitlines()
914
+ section_index = find_section(lines, f"## {heading}")
915
+ if section_index is None:
916
+ return None
917
+ values = {}
918
+ for line in lines[section_index + 1 :]:
919
+ if line.startswith("## "):
920
+ break
921
+ if ":" not in line:
922
+ continue
923
+ key, value = line.split(":", 1)
924
+ normalized_key = key.strip().lower().replace(" ", "_")
925
+ values[normalized_key] = value.strip()
926
+ return values
927
+
928
+
929
+ def phase_number_from_text(value):
930
+ match = re.search(r"\bphase[-_\s]*(?P<number>\d+)\b", value, flags=re.IGNORECASE)
931
+ if not match:
932
+ return None
933
+ return match.group("number")
934
+
935
+
936
+ def plan_title(text):
937
+ for line in text.splitlines():
938
+ if line.startswith("# Execution Plan:"):
939
+ return line.split(":", 1)[1].strip()
940
+ return ""
941
+
942
+
943
+ def default_workstream_id_from_plan(plan_path, text):
944
+ source = plan_path.stem
945
+ source = re.sub(r"^\d{4}-\d{2}-\d{2}-", "", source)
946
+ source = re.sub(r"phase[-_\s]*\d+", "", source, flags=re.IGNORECASE)
947
+ source = source.strip("-_ ")
948
+ if not source:
949
+ source = plan_title(text)
950
+ source = re.sub(r"phase[-_\s]*\d+", "", source, flags=re.IGNORECASE)
951
+ return slugify(source or "workstream")
952
+
953
+
954
+ def phase_continuity_for_plan(plan_path, text):
955
+ values = section_key_values(text, "Phase Continuity")
956
+ detected_phase = phase_number_from_text(plan_path.stem) or phase_number_from_text(plan_title(text))
957
+ if values is None:
958
+ return {
959
+ "status": "missing",
960
+ "detected_phase": detected_phase,
961
+ "mode": None,
962
+ "workstream": None,
963
+ "current_phase": None,
964
+ "next_phase": None,
965
+ "continuation": None,
966
+ "next_action": None,
967
+ "closure_reason": None,
968
+ "resume_notes": None,
969
+ }
970
+ mode = values.get("mode", "").lower()
971
+ workstream = values.get("workstream")
972
+ current_phase = values.get("current_phase")
973
+ next_phase = values.get("next_phase")
974
+ continuation = values.get("continuation")
975
+ next_action = values.get("next_action")
976
+ closure_reason = values.get("closure_reason")
977
+ resume_notes = values.get("resume_notes")
978
+ return {
979
+ "status": "present",
980
+ "detected_phase": detected_phase,
981
+ "mode": mode,
982
+ "workstream": workstream,
983
+ "current_phase": current_phase,
984
+ "next_phase": next_phase,
985
+ "continuation": continuation,
986
+ "next_action": next_action,
987
+ "closure_reason": closure_reason,
988
+ "resume_notes": resume_notes,
989
+ }
990
+
991
+
992
+ def is_empty_continuity_value(value):
993
+ if value is None:
994
+ return True
995
+ return value.strip().lower() in {"", "none", "pending", "unknown", "n/a", "-"}
996
+
997
+
998
+ def phase_continuity_issues(repo, plan_path, plan_text):
999
+ continuity = phase_continuity_for_plan(plan_path, plan_text)
1000
+ detected_phase = continuity["detected_phase"]
1001
+ if continuity["status"] == "missing":
1002
+ if detected_phase:
1003
+ return [
1004
+ {
1005
+ "severity": "error",
1006
+ "code": "missing-phase-continuity",
1007
+ "path": str(plan_path.relative_to(repo)),
1008
+ "message": "Phased plan is missing a Phase Continuity section.",
1009
+ }
1010
+ ]
1011
+ return []
1012
+ mode = continuity["mode"]
1013
+ if mode in {"single-phase", "single", "none"} and not detected_phase:
1014
+ return []
1015
+ issues = []
1016
+ relative_plan = str(plan_path.relative_to(repo))
1017
+ if mode not in {"multi-phase", "phased", "paused", "completed", "stopped"} and detected_phase:
1018
+ issues.append(
1019
+ {
1020
+ "severity": "error",
1021
+ "code": "phase-mode-not-declared",
1022
+ "path": relative_plan,
1023
+ "message": "Plan name indicates a phase, but Phase Continuity does not declare multi-phase, paused, completed, or stopped mode.",
1024
+ }
1025
+ )
1026
+ if is_empty_continuity_value(continuity["workstream"]):
1027
+ issues.append(
1028
+ {
1029
+ "severity": "error",
1030
+ "code": "missing-workstream",
1031
+ "path": relative_plan,
1032
+ "message": "Phased or multi-plan work must name a workstream in Phase Continuity.",
1033
+ }
1034
+ )
1035
+ if is_empty_continuity_value(continuity["current_phase"]):
1036
+ issues.append(
1037
+ {
1038
+ "severity": "error",
1039
+ "code": "missing-current-phase",
1040
+ "path": relative_plan,
1041
+ "message": "Phased or multi-plan work must record the current phase.",
1042
+ }
1043
+ )
1044
+ continuation = continuity["continuation"]
1045
+ closure_reason = continuity["closure_reason"]
1046
+ next_action = continuity["next_action"]
1047
+ if mode in {"completed", "stopped"}:
1048
+ if is_empty_continuity_value(closure_reason):
1049
+ issues.append(
1050
+ {
1051
+ "severity": "error",
1052
+ "code": "missing-phase-closure-reason",
1053
+ "path": relative_plan,
1054
+ "message": "Completed or stopped workstreams must explain why no next phase is needed.",
1055
+ }
1056
+ )
1057
+ return issues
1058
+ if is_empty_continuity_value(continuation):
1059
+ issues.append(
1060
+ {
1061
+ "severity": "error",
1062
+ "code": "missing-continuation",
1063
+ "path": relative_plan,
1064
+ "message": "Multi-phase work must point to a next active plan, workstreams ledger, tech debt item, or explicit closure.",
1065
+ }
1066
+ )
1067
+ elif "workstreams.md" in continuation and not is_empty_continuity_value(continuity["workstream"]):
1068
+ ledger = workstreams_path(repo)
1069
+ if not ledger.exists() or continuity["workstream"] not in ledger.read_text():
1070
+ issues.append(
1071
+ {
1072
+ "severity": "error",
1073
+ "code": "missing-workstream-ledger-entry",
1074
+ "path": relative_plan,
1075
+ "message": "Phase Continuity points to workstreams.md, but the named workstream is not recorded there.",
1076
+ }
1077
+ )
1078
+ if is_empty_continuity_value(next_action):
1079
+ issues.append(
1080
+ {
1081
+ "severity": "error",
1082
+ "code": "missing-next-action",
1083
+ "path": relative_plan,
1084
+ "message": "Multi-phase work must record a concrete next action for recovery.",
1085
+ }
1086
+ )
1087
+ return issues
1088
+
1089
+
1090
+ def open_defects_for_plan(text):
1091
+ open_items = []
1092
+ for item in extract_defect_items(text):
1093
+ parsed = parse_defect_item(item)
1094
+ if parsed and parsed["status"] == "open":
1095
+ open_items.append(parsed)
1096
+ return open_items
1097
+
1098
+
1099
+ def render_quality_gate(scores, notes, minimum, open_defects=None):
1100
+ open_defects = open_defects or []
1101
+ average = sum(scores.values()) / len(scores)
1102
+ low_dimensions = [
1103
+ label for key, label in QUALITY_DIMENSIONS if scores[key] < minimum
1104
+ ]
1105
+ passed = average >= minimum and not low_dimensions and not open_defects
1106
+ status = "pass" if passed else "fail"
1107
+ lines = [
1108
+ f"Status: {status}",
1109
+ f"Minimum score: {minimum:.1f}",
1110
+ f"Average score: {average:.1f}",
1111
+ f"Last scored: {datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}",
1112
+ "",
1113
+ "| Dimension | Score | Notes |",
1114
+ "| --- | ---: | --- |",
1115
+ ]
1116
+ for key, label in QUALITY_DIMENSIONS:
1117
+ note = notes.get(key) or "No note provided."
1118
+ safe_note = note.replace("\n", " ").replace("|", "\\|").strip()
1119
+ lines.append(f"| {label} | {scores[key]:.1f} | {safe_note} |")
1120
+ return "\n".join(lines), passed, average, low_dimensions
1121
+
1122
+
1123
+ def render_rework_section(passed, average, minimum, low_dimensions, notes, open_defects=None):
1124
+ open_defects = open_defects or []
1125
+ if passed:
1126
+ return "None. Quality gate passed."
1127
+ lines = [
1128
+ f"- Rework implementation until every quality dimension is at least {minimum:.1f}; current average is {average:.1f}.",
1129
+ ]
1130
+ for defect in open_defects:
1131
+ evidence = f" Evidence: {defect['evidence']}." if defect.get("evidence") else ""
1132
+ lines.append(
1133
+ f"- Resolve {defect['id']} ({defect['severity']}): {defect['summary']}.{evidence}"
1134
+ )
1135
+ for key, label in QUALITY_DIMENSIONS:
1136
+ if label in low_dimensions:
1137
+ note = notes.get(key) or "No note provided."
1138
+ lines.append(f"- Improve {label}: {note}")
1139
+ return "\n".join(lines)
1140
+
1141
+
1142
+ def update_quality_gate(plan_path, scores, notes, minimum):
1143
+ text = plan_path.read_text()
1144
+ open_defects = open_defects_for_plan(text)
1145
+ gate_text, passed, average, low_dimensions = render_quality_gate(scores, notes, minimum, open_defects)
1146
+ updated = replace_section(text, "Quality Gate", gate_text)
1147
+ updated = replace_section(
1148
+ updated,
1149
+ "Rework Required",
1150
+ render_rework_section(passed, average, minimum, low_dimensions, notes, open_defects),
1151
+ )
1152
+ plan_path.write_text(updated)
1153
+ return {
1154
+ "status": "pass" if passed else "fail",
1155
+ "minimum": minimum,
1156
+ "average": round(average, 1),
1157
+ "low_dimensions": low_dimensions,
1158
+ "open_defects": [defect["id"] for defect in open_defects],
1159
+ }
1160
+
1161
+
1162
+ def missing_quality_notes(notes):
1163
+ missing = []
1164
+ for key, label in QUALITY_DIMENSIONS:
1165
+ if not (notes.get(key) or "").strip():
1166
+ missing.append(
1167
+ {
1168
+ "dimension": label,
1169
+ "argument": "--" + QUALITY_NOTE_ARGS[key],
1170
+ "message": f"Provide evidence for {label}.",
1171
+ }
1172
+ )
1173
+ return missing
1174
+
1175
+
1176
+ def assert_quality_gate_passed(plan_text):
1177
+ open_defects = open_defects_for_plan(plan_text)
1178
+ if open_defects:
1179
+ defects = "\n".join(
1180
+ f"- {defect['id']} ({defect['severity']}): {defect['summary']}" for defect in open_defects
1181
+ )
1182
+ raise RuntimeError(
1183
+ "Cannot close plan with unresolved defects:\n"
1184
+ + defects
1185
+ + "\nRun `defect-resolve`, re-run validation, and score again."
1186
+ )
1187
+ gate = quality_gate_for_plan(plan_text)
1188
+ if gate["status"] != "pass":
1189
+ raise RuntimeError(
1190
+ "Cannot close plan until the quality gate passes. "
1191
+ "Run `quality-score`, fix any `## Rework Required` items, then score again."
1192
+ )
1193
+ return gate
1194
+
1195
+
1196
+ def render_phase_continuity(mode, workstream, current_phase, next_phase, continuation, next_action, closure_reason, resume_notes):
1197
+ return "\n".join(
1198
+ [
1199
+ f"Mode: {mode}",
1200
+ f"Workstream: {workstream}",
1201
+ f"Current phase: {current_phase}",
1202
+ f"Next phase: {next_phase}",
1203
+ f"Continuation: {continuation}",
1204
+ f"Next action: {next_action}",
1205
+ f"Closure reason: {closure_reason}",
1206
+ f"Resume notes: {resume_notes}",
1207
+ ]
1208
+ )
1209
+
1210
+
1211
+ def update_phase_continuity(plan_path, mode, workstream, current_phase, next_phase, continuation, next_action, closure_reason, resume_notes):
1212
+ text = plan_path.read_text()
1213
+ detected_phase = phase_number_from_text(plan_path.stem) or phase_number_from_text(plan_title(text)) or "none"
1214
+ resolved_workstream = workstream or default_workstream_id_from_plan(plan_path, text)
1215
+ resolved_current_phase = current_phase or detected_phase
1216
+ body = render_phase_continuity(
1217
+ mode,
1218
+ resolved_workstream,
1219
+ resolved_current_phase,
1220
+ next_phase,
1221
+ continuation,
1222
+ next_action,
1223
+ closure_reason,
1224
+ resume_notes,
1225
+ )
1226
+ plan_path.write_text(replace_section(text, "Phase Continuity", body))
1227
+ return {
1228
+ "status": "updated",
1229
+ "mode": mode,
1230
+ "workstream": resolved_workstream,
1231
+ "current_phase": resolved_current_phase,
1232
+ "next_phase": next_phase,
1233
+ "continuation": continuation,
1234
+ "next_action": next_action,
1235
+ }
1236
+
1237
+
1238
+ def workstreams_path(repo):
1239
+ return repo / "docs" / "exec-plans" / "workstreams.md"
1240
+
1241
+
1242
+ def workstream_table_insert_index(lines):
1243
+ index_heading = find_section(lines, "## Index")
1244
+ if index_heading is None:
1245
+ return len(lines)
1246
+ index = index_heading + 1
1247
+ while index < len(lines) and lines[index].strip() == "":
1248
+ index += 1
1249
+ while index < len(lines) and not lines[index].startswith("| ID |"):
1250
+ if lines[index].startswith("## "):
1251
+ return index
1252
+ index += 1
1253
+ if index >= len(lines):
1254
+ return index_heading + 1
1255
+ index += 1
1256
+ if index < len(lines) and lines[index].startswith("| ---"):
1257
+ index += 1
1258
+ while index < len(lines) and lines[index].startswith("|"):
1259
+ index += 1
1260
+ return index
1261
+
1262
+
1263
+ def append_workstream_entry(repo, workstream_id, status, current_plan, last_completed_plan, next_action, goal, resume_notes):
1264
+ target = workstreams_path(repo)
1265
+ ensure_parent(target)
1266
+ if not target.exists():
1267
+ target.write_text(DOC_FILES["docs/exec-plans/workstreams.md"].format(marker=MANAGED_MARKER))
1268
+ text = target.read_text()
1269
+ today = datetime.now(UTC).strftime("%Y-%m-%d")
1270
+ row = (
1271
+ f"| {workstream_id} | {status} | {current_plan or 'none'} | "
1272
+ f"{last_completed_plan or 'none'} | {next_action or 'none'} | {today} |"
1273
+ )
1274
+ lines = text.splitlines()
1275
+ replaced = False
1276
+ updated_lines = []
1277
+ for line in lines:
1278
+ if line.startswith(f"| {workstream_id} |"):
1279
+ updated_lines.append(row)
1280
+ replaced = True
1281
+ else:
1282
+ updated_lines.append(line)
1283
+ if not replaced:
1284
+ insert_index = workstream_table_insert_index(updated_lines)
1285
+ updated_lines.insert(insert_index, row)
1286
+ detail = (
1287
+ f"Status: {status}\n"
1288
+ f"Goal: {goal or 'Record the durable goal for this workstream.'}\n"
1289
+ f"Current plan: {current_plan or 'none'}\n"
1290
+ f"Last completed plan: {last_completed_plan or 'none'}\n"
1291
+ f"Next action: {next_action or 'none'}\n"
1292
+ f"Resume notes: {resume_notes or 'Read the current or last completed plan before continuing.'}\n"
1293
+ f"Last updated: {today}"
1294
+ )
1295
+ updated_text = "\n".join(updated_lines).rstrip() + "\n"
1296
+ updated_text = replace_section(updated_text, workstream_id, detail)
1297
+ target.write_text(updated_text)
1298
+ return target
1299
+
1300
+
1301
+ def update_workstreams_after_plan_close(repo, active_relative_plan, completed_relative_plan):
1302
+ target = workstreams_path(repo)
1303
+ if not target.exists():
1304
+ return
1305
+ lines = target.read_text().splitlines()
1306
+ updated = []
1307
+ current_plan_was_closed = False
1308
+ for line in lines:
1309
+ stripped = line.strip()
1310
+ if stripped.startswith("|") and not stripped.startswith("| ---") and not stripped.startswith("| ID |"):
1311
+ cells = [cell.strip() for cell in stripped.strip("|").split("|")]
1312
+ if len(cells) == 6:
1313
+ if cells[2] == active_relative_plan:
1314
+ cells[2] = "none"
1315
+ if cells[3] == "none":
1316
+ cells[3] = completed_relative_plan
1317
+ if cells[3] == active_relative_plan:
1318
+ cells[3] = completed_relative_plan
1319
+ updated.append("| " + " | ".join(cells) + " |")
1320
+ continue
1321
+ if line == f"Current plan: {active_relative_plan}":
1322
+ updated.append("Current plan: none")
1323
+ current_plan_was_closed = True
1324
+ continue
1325
+ if line == f"Last completed plan: {active_relative_plan}":
1326
+ updated.append(f"Last completed plan: {completed_relative_plan}")
1327
+ current_plan_was_closed = False
1328
+ continue
1329
+ if current_plan_was_closed and line == "Last completed plan: none":
1330
+ updated.append(f"Last completed plan: {completed_relative_plan}")
1331
+ current_plan_was_closed = False
1332
+ continue
1333
+ updated.append(line)
1334
+ if line.startswith("## "):
1335
+ current_plan_was_closed = False
1336
+ target.write_text("\n".join(updated).rstrip() + "\n")
1337
+
1338
+
1339
+ def assert_phase_continuity_closed(repo, plan_path, plan_text):
1340
+ issues = phase_continuity_issues(repo, plan_path, plan_text)
1341
+ if issues:
1342
+ messages = "\n".join(f"- {issue['code']}: {issue['message']}" for issue in issues)
1343
+ raise RuntimeError(
1344
+ "Cannot close plan until phase continuity is recorded:\n"
1345
+ + messages
1346
+ + "\nRun `phase-set` and update `workstreams.md` or `tech-debt-tracker.md` before closing."
1347
+ )
1348
+
1349
+
1350
+ def append_knowledge_item(plan_path, fact, destination):
1351
+ text = plan_path.read_text()
1352
+ lines = text.splitlines()
1353
+ section_index = find_section(lines, "## Durable Knowledge To Capture")
1354
+ if section_index is None:
1355
+ raise ValueError("Plan is missing '## Durable Knowledge To Capture'")
1356
+ filtered_lines = [line for line in lines if line.strip() != DEFAULT_KNOWLEDGE_PLACEHOLDER]
1357
+ insert_index = section_index + 1
1358
+ while insert_index < len(filtered_lines) and not filtered_lines[insert_index].startswith("## "):
1359
+ insert_index += 1
1360
+ item_id = knowledge_id_for(fact, destination)
1361
+ item = f"- [ ] [id:{item_id}] {fact} -> {destination}"
1362
+ updated_lines = filtered_lines[:insert_index] + [item] + filtered_lines[insert_index:]
1363
+ plan_path.write_text("\n".join(updated_lines).rstrip() + "\n")
1364
+ return item, item_id
1365
+
1366
+
1367
+ def render_open_defect_rework(open_defects):
1368
+ lines = ["- Resolve all open defects, then re-run validation and `quality-score`."]
1369
+ for defect in open_defects:
1370
+ evidence = f" Evidence: {defect['evidence']}." if defect.get("evidence") else ""
1371
+ lines.append(f"- Resolve {defect['id']} ({defect['severity']}): {defect['summary']}.{evidence}")
1372
+ return "\n".join(lines)
1373
+
1374
+
1375
+ def mark_quality_gate_blocked_by_defects(text):
1376
+ open_defects = open_defects_for_plan(text)
1377
+ if not open_defects:
1378
+ return text
1379
+ lines = text.splitlines()
1380
+ section_index = find_section(lines, "## Quality Gate")
1381
+ if section_index is None:
1382
+ gate_text = "\n".join(
1383
+ [
1384
+ "Status: fail",
1385
+ "Minimum score: 8.0",
1386
+ "Average score: pending",
1387
+ f"Last scored: {datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}",
1388
+ "",
1389
+ "Blocked by unresolved defects. Run `defect-resolve`, re-run validation, then run `quality-score`.",
1390
+ ]
1391
+ )
1392
+ text = replace_section(text, "Quality Gate", gate_text)
1393
+ else:
1394
+ end_index = len(lines)
1395
+ for index in range(section_index + 1, len(lines)):
1396
+ if lines[index].startswith("## "):
1397
+ end_index = index
1398
+ break
1399
+ section_lines = lines[section_index + 1 : end_index]
1400
+ has_status = False
1401
+ updated_section = []
1402
+ for line in section_lines:
1403
+ if line.startswith("Status:"):
1404
+ updated_section.append("Status: fail")
1405
+ has_status = True
1406
+ elif line.startswith("Last scored:"):
1407
+ updated_section.append(f"Last scored: {datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}")
1408
+ else:
1409
+ updated_section.append(line)
1410
+ if not has_status:
1411
+ updated_section.insert(0, "Status: fail")
1412
+ lines = lines[: section_index + 1] + updated_section + lines[end_index:]
1413
+ text = "\n".join(lines).rstrip() + "\n"
1414
+ return replace_section(text, "Rework Required", render_open_defect_rework(open_defects))
1415
+
1416
+
1417
+ def append_defect_item(plan_path, severity, summary, evidence=None):
1418
+ text = plan_path.read_text()
1419
+ if find_section(text.splitlines(), "## Defects To Resolve") is None:
1420
+ text = replace_section(text, "Defects To Resolve", DEFAULT_DEFECT_PLACEHOLDER)
1421
+ lines = text.splitlines()
1422
+ section_index = find_section(lines, "## Defects To Resolve")
1423
+ if section_index is None:
1424
+ raise ValueError("Plan is missing '## Defects To Resolve'")
1425
+ filtered_lines = [line for line in lines if line.strip() != DEFAULT_DEFECT_PLACEHOLDER]
1426
+ insert_index = section_index + 1
1427
+ while insert_index < len(filtered_lines) and not filtered_lines[insert_index].startswith("## "):
1428
+ insert_index += 1
1429
+ item_id = defect_id_for(summary)
1430
+ safe_summary = clean_fact_text(summary)
1431
+ safe_evidence = clean_fact_text(evidence) if evidence else None
1432
+ item = f"- [ ] [bug:{item_id}] [{severity}] {safe_summary}"
1433
+ if safe_evidence:
1434
+ item = f"{item} | evidence: {safe_evidence}"
1435
+ updated_lines = filtered_lines[:insert_index] + [item] + filtered_lines[insert_index:]
1436
+ plan_path.write_text(mark_quality_gate_blocked_by_defects("\n".join(updated_lines).rstrip() + "\n"))
1437
+ return item, item_id
1438
+
1439
+
1440
+ def close_defect_line(line, fix_evidence):
1441
+ updated = line.replace("- [ ]", "- [x]", 1)
1442
+ if "| fix:" not in updated:
1443
+ updated = f"{updated} | fix: {fix_evidence}"
1444
+ return updated
1445
+
1446
+
1447
+ def mark_defect_resolved(plan_path, defect_id, fix_evidence):
1448
+ if not defect_id:
1449
+ raise ValueError("Provide --id to resolve a defect")
1450
+ if not fix_evidence:
1451
+ raise ValueError("Provide --fix-evidence or --fix-evidence-file to resolve a defect")
1452
+ lines = plan_path.read_text().splitlines()
1453
+ safe_fix = clean_fact_text(fix_evidence)
1454
+ replaced = False
1455
+ updated = []
1456
+ for line in lines:
1457
+ stripped = line.strip()
1458
+ parsed = parse_defect_item(stripped)
1459
+ if parsed and parsed["status"] == "open" and parsed["id"] == defect_id and not replaced:
1460
+ updated.append(close_defect_line(line, safe_fix))
1461
+ replaced = True
1462
+ else:
1463
+ updated.append(line)
1464
+ if not replaced:
1465
+ raise ValueError(f"Open defect not found for id: {defect_id}")
1466
+ text = "\n".join(updated).rstrip() + "\n"
1467
+ open_defects = open_defects_for_plan(text)
1468
+ if open_defects:
1469
+ text = replace_section(text, "Rework Required", render_open_defect_rework(open_defects))
1470
+ else:
1471
+ text = replace_section(
1472
+ text,
1473
+ "Rework Required",
1474
+ "Defects resolved. Re-run validation and `quality-score` before closing.",
1475
+ )
1476
+ plan_path.write_text(text)
1477
+
1478
+
1479
+ def mark_knowledge_items_closed(text):
1480
+ lines = text.splitlines()
1481
+ updated = []
1482
+ in_knowledge_section = False
1483
+ for line in lines:
1484
+ if line.startswith("## "):
1485
+ in_knowledge_section = line.strip().lower() == "## durable knowledge to capture"
1486
+ if in_knowledge_section and line.strip().startswith("- [ ]") and line.strip() != DEFAULT_KNOWLEDGE_PLACEHOLDER:
1487
+ updated.append(line.replace("- [ ]", "- [x]", 1))
1488
+ else:
1489
+ updated.append(line)
1490
+ return "\n".join(updated).rstrip() + "\n"
1491
+
1492
+
1493
+ def destination_contains_fact(repo, destination, fact):
1494
+ target = repo / destination
1495
+ if not target.exists() or not target.is_file():
1496
+ return False
1497
+ try:
1498
+ return normalize_fact_for_match(fact) in normalize_fact_for_match(target.read_text())
1499
+ except UnicodeDecodeError:
1500
+ return False
1501
+
1502
+
1503
+ def normalize_fact_for_match(value):
1504
+ normalized = value.replace("`", "")
1505
+ normalized = re.sub(r"\s+", " ", normalized)
1506
+ normalized = normalized.strip()
1507
+ normalized = re.sub(r"[.。]+$", "", normalized)
1508
+ return normalized
1509
+
1510
+
1511
+ def append_fact_to_destination(repo, destination, fact):
1512
+ target = repo / destination
1513
+ ensure_parent(target)
1514
+ existing = ""
1515
+ if target.exists():
1516
+ existing = target.read_text()
1517
+ separator = "\n" if existing.endswith("\n") or not existing else "\n\n"
1518
+ target.write_text(existing + separator + fact + "\n")
1519
+
1520
+
1521
+ def close_knowledge_line(line, evidence=None):
1522
+ updated = line.replace("- [ ]", "- [x]", 1)
1523
+ if evidence and "| evidence:" not in updated:
1524
+ updated = f"{updated} | evidence: {evidence}"
1525
+ return updated
1526
+
1527
+
1528
+ def mark_single_knowledge_item_written(
1529
+ repo,
1530
+ plan_path,
1531
+ fact_text=None,
1532
+ destination=None,
1533
+ append=False,
1534
+ knowledge_id=None,
1535
+ evidence=None,
1536
+ ):
1537
+ if not fact_text and not knowledge_id:
1538
+ raise ValueError("Provide either --id or --fact to mark knowledge as written")
1539
+ lines = plan_path.read_text().splitlines()
1540
+ target = clean_fact_text(fact_text) if fact_text else None
1541
+ target_destination = clean_destination_text(destination) if destination else None
1542
+ target_evidence = clean_fact_text(evidence) if evidence else None
1543
+ replaced = False
1544
+ updated = []
1545
+ for line in lines:
1546
+ stripped = line.strip()
1547
+ parsed = parse_knowledge_item(stripped)
1548
+ if not parsed:
1549
+ updated.append(line)
1550
+ continue
1551
+ destination_matches = target_destination is None or parsed["destination"] == target_destination
1552
+ fact_matches = target is not None and normalize_fact_for_match(target) == normalize_fact_for_match(parsed["fact"])
1553
+ id_matches = knowledge_id is not None and parsed["id"] == knowledge_id
1554
+ if stripped.startswith("- [ ]") and (id_matches or fact_matches) and destination_matches and not replaced:
1555
+ parsed_destination = parsed["destination"]
1556
+ if not parsed_destination:
1557
+ raise ValueError("Destination is required to verify durable knowledge")
1558
+ verification_text = target_evidence or target or parsed["fact"]
1559
+ if not destination_contains_fact(repo, parsed_destination, verification_text):
1560
+ if append:
1561
+ append_fact_to_destination(repo, parsed_destination, verification_text)
1562
+ else:
1563
+ raise ValueError(
1564
+ f"Destination {parsed_destination} does not contain verification text: {verification_text}. "
1565
+ "Write it there first, pass --evidence with text present in the doc, or re-run with --append."
1566
+ )
1567
+ updated.append(close_knowledge_line(line, evidence=target_evidence))
1568
+ replaced = True
1569
+ else:
1570
+ updated.append(line)
1571
+ if not replaced:
1572
+ target_description = f"id: {knowledge_id}" if knowledge_id else f"fact: {fact_text}"
1573
+ raise ValueError(f"Open knowledge item not found for {target_description}")
1574
+ plan_path.write_text("\n".join(updated).rstrip() + "\n")
1575
+
1576
+
1577
+ def should_write(path, refresh_managed, force):
1578
+ if not path.exists():
1579
+ return True
1580
+ if force:
1581
+ return True
1582
+ try:
1583
+ is_managed = is_managed_text(path.read_text())
1584
+ except UnicodeDecodeError:
1585
+ return False
1586
+ if refresh_managed and is_managed:
1587
+ return True
1588
+ return False
1589
+
1590
+
1591
+ def write_scaffold(repo, analysis, answers, refresh_managed=False, force=False):
1592
+ written = []
1593
+ created = []
1594
+ refreshed = []
1595
+ skipped = []
1596
+ all_templates = {}
1597
+ all_templates.update(ROOT_FILES)
1598
+ all_templates.update(DOC_FILES)
1599
+
1600
+ for relative_path, template in all_templates.items():
1601
+ target = repo / relative_path
1602
+ existed = target.exists()
1603
+ if should_write(target, refresh_managed, force):
1604
+ ensure_parent(target)
1605
+ content = fill_template(template, answers, analysis)
1606
+ target.write_text(content)
1607
+ written.append(relative_path)
1608
+ if existed:
1609
+ refreshed.append(relative_path)
1610
+ else:
1611
+ created.append(relative_path)
1612
+ else:
1613
+ skipped.append(relative_path)
1614
+ return written, skipped, created, refreshed
1615
+
1616
+
1617
+ def active_plan_dir(repo):
1618
+ return repo / "docs" / "exec-plans" / "active"
1619
+
1620
+
1621
+ def completed_plan_dir(repo):
1622
+ return repo / "docs" / "exec-plans" / "completed"
1623
+
1624
+
1625
+ def plan_path_from_arg(repo, plan_arg):
1626
+ raw_plan = Path(plan_arg)
1627
+ if raw_plan.is_absolute():
1628
+ plan_path = raw_plan.resolve()
1629
+ else:
1630
+ plan_path = (repo / raw_plan).resolve()
1631
+
1632
+ try:
1633
+ relative_plan = str(plan_path.relative_to(repo.resolve()))
1634
+ except ValueError as error:
1635
+ raise ValueError(f"Plan must be inside repo: {plan_arg}") from error
1636
+
1637
+ if not plan_path.exists():
1638
+ raise FileNotFoundError(f"Plan not found: {plan_path}")
1639
+
1640
+ return plan_path, relative_plan
1641
+
1642
+
1643
+ def create_plan(repo, slug, goal):
1644
+ plan_dir = active_plan_dir(repo)
1645
+ plan_dir.mkdir(parents=True, exist_ok=True)
1646
+ filename = f"{datetime.now(UTC).strftime('%Y-%m-%d')}-{slugify(slug)}.md"
1647
+ plan_path = plan_dir / filename
1648
+ if plan_path.exists():
1649
+ raise FileExistsError(f"Plan already exists: {plan_path}")
1650
+ title = slug.replace("-", " ").strip() or "task"
1651
+ content = PLAN_TEMPLATE.format(
1652
+ title=title.title(),
1653
+ goal=goal,
1654
+ defect_section=DEFAULT_DEFECT_PLACEHOLDER,
1655
+ knowledge_section="- [ ] Add durable facts here as they emerge -> <destination-doc>",
1656
+ )
1657
+ plan_path.write_text(content)
1658
+ return plan_path
1659
+
1660
+
1661
+ def close_plan(repo, plan_relative_path, summary, force):
1662
+ plan_path, active_relative_path = plan_path_from_arg(repo, plan_relative_path)
1663
+ text = plan_path.read_text()
1664
+ if not force:
1665
+ assert_quality_gate_passed(text)
1666
+ assert_phase_continuity_closed(repo, plan_path, text)
1667
+ open_items = [
1668
+ item
1669
+ for item in extract_knowledge_items(text)
1670
+ if item.startswith("- [ ]") and item != DEFAULT_KNOWLEDGE_PLACEHOLDER
1671
+ ]
1672
+ if open_items and not force:
1673
+ raise RuntimeError(
1674
+ "Cannot close plan with unresolved durable knowledge items:\n" + "\n".join(open_items)
1675
+ )
1676
+ updated_text = replace_completion_notes(mark_knowledge_items_closed(text), summary)
1677
+ completed_dir = completed_plan_dir(repo)
1678
+ completed_dir.mkdir(parents=True, exist_ok=True)
1679
+ destination = completed_dir / plan_path.name
1680
+ destination.write_text(updated_text)
1681
+ plan_path.unlink()
1682
+ completed_relative_path = str(destination.relative_to(repo))
1683
+ update_workstreams_after_plan_close(repo, active_relative_path, completed_relative_path)
1684
+ return destination, open_items
1685
+
1686
+
1687
+ def check_harness(repo):
1688
+ required_files = [
1689
+ "AGENTS.md",
1690
+ "ARCHITECTURE.md",
1691
+ "docs/PLANS.md",
1692
+ "docs/QUALITY_SCORE.md",
1693
+ "docs/RELIABILITY.md",
1694
+ "docs/SECURITY.md",
1695
+ "docs/exec-plans/workstreams.md",
1696
+ "docs/exec-plans/active/README.md",
1697
+ "docs/exec-plans/active/_template.md",
1698
+ "docs/exec-plans/completed/README.md",
1699
+ "docs/sops/encode-unseen-knowledge.md",
1700
+ ]
1701
+ issues = []
1702
+ for relative_path in required_files:
1703
+ if not (repo / relative_path).exists():
1704
+ issues.append(
1705
+ {
1706
+ "severity": "error",
1707
+ "code": "missing-required-file",
1708
+ "path": relative_path,
1709
+ "message": f"Required harness file is missing: {relative_path}",
1710
+ }
1711
+ )
1712
+
1713
+ active_dir = active_plan_dir(repo)
1714
+ if active_dir.exists():
1715
+ for plan_path in sorted(active_dir.glob("*.md")):
1716
+ if plan_path.name in {"README.md", "_template.md"}:
1717
+ continue
1718
+ relative_plan = str(plan_path.relative_to(repo))
1719
+ quality_gate = quality_gate_for_plan(plan_path.read_text())
1720
+ if quality_gate["status"] == "missing":
1721
+ issues.append(
1722
+ {
1723
+ "severity": "error",
1724
+ "code": "missing-quality-gate",
1725
+ "path": relative_plan,
1726
+ "message": "Active plan is missing a Quality Gate section.",
1727
+ }
1728
+ )
1729
+ elif quality_gate["status"] != "pass":
1730
+ issues.append(
1731
+ {
1732
+ "severity": "error",
1733
+ "code": "quality-gate-not-passing",
1734
+ "path": relative_plan,
1735
+ "message": "Active plan quality gate has not passed; score the work and finish rework before handoff.",
1736
+ }
1737
+ )
1738
+ for defect in open_defects_for_plan(plan_path.read_text()):
1739
+ issues.append(
1740
+ {
1741
+ "severity": "error",
1742
+ "code": "open-defect",
1743
+ "path": relative_plan,
1744
+ "id": defect["id"],
1745
+ "defect_severity": defect["severity"],
1746
+ "message": f"Active plan has an unresolved defect: {defect['summary']}",
1747
+ }
1748
+ )
1749
+ issues.extend(phase_continuity_issues(repo, plan_path, plan_path.read_text()))
1750
+ for item in extract_knowledge_items(plan_path.read_text()):
1751
+ if item == DEFAULT_KNOWLEDGE_PLACEHOLDER:
1752
+ continue
1753
+ parsed = parse_knowledge_item(item)
1754
+ if not parsed:
1755
+ issues.append(
1756
+ {
1757
+ "severity": "error",
1758
+ "code": "unparseable-knowledge-item",
1759
+ "path": relative_plan,
1760
+ "message": f"Knowledge item is not parseable: {item}",
1761
+ }
1762
+ )
1763
+ continue
1764
+ if parsed["status"] == "open":
1765
+ issues.append(
1766
+ {
1767
+ "severity": "error",
1768
+ "code": "open-durable-knowledge",
1769
+ "path": relative_plan,
1770
+ "destination": parsed["destination"],
1771
+ "message": f"Durable knowledge is still open: {parsed['fact']}",
1772
+ }
1773
+ )
1774
+ else:
1775
+ verification_text = parsed["evidence"] or parsed["fact"]
1776
+ if destination_contains_fact(repo, parsed["destination"], verification_text):
1777
+ continue
1778
+ issues.append(
1779
+ {
1780
+ "severity": "error",
1781
+ "code": "missing-written-knowledge",
1782
+ "path": relative_plan,
1783
+ "destination": parsed["destination"],
1784
+ "message": f"Marked knowledge evidence is missing from destination: {verification_text}",
1785
+ }
1786
+ )
1787
+
1788
+ ledger = workstreams_path(repo)
1789
+ if ledger.exists():
1790
+ for index, line in enumerate(ledger.read_text().splitlines(), start=1):
1791
+ stripped = line.strip()
1792
+ if not stripped.startswith("|") or stripped.startswith("| ---") or stripped.startswith("| ID |"):
1793
+ continue
1794
+ cells = [cell.strip() for cell in stripped.strip("|").split("|")]
1795
+ if len(cells) != 6:
1796
+ continue
1797
+ workstream_id, _, current_plan, last_completed_plan, _, _ = cells
1798
+ for label, plan_value in [
1799
+ ("current plan", current_plan),
1800
+ ("last completed plan", last_completed_plan),
1801
+ ]:
1802
+ if plan_value in {"", "none", "n/a", "-"}:
1803
+ continue
1804
+ if not (repo / plan_value).exists():
1805
+ issues.append(
1806
+ {
1807
+ "severity": "error",
1808
+ "code": "missing-workstream-plan-reference",
1809
+ "path": str(ledger.relative_to(repo)),
1810
+ "line": index,
1811
+ "workstream": workstream_id,
1812
+ "message": f"Workstream {workstream_id} references missing {label}: {plan_value}",
1813
+ }
1814
+ )
1815
+
1816
+ return {
1817
+ "repo": str(repo),
1818
+ "status": "pass" if not issues else "fail",
1819
+ "issue_count": len(issues),
1820
+ "issues": issues,
1821
+ }
1822
+
1823
+
1824
+ def docs_text_for_reference_scan(repo):
1825
+ docs_root = repo / "docs"
1826
+ chunks = []
1827
+ roots = [repo / "AGENTS.md", repo / "ARCHITECTURE.md"]
1828
+ if docs_root.exists():
1829
+ roots.extend(path for path in docs_root.rglob("*") if path.is_file())
1830
+ for path in roots:
1831
+ if not path.exists() or not path.is_file():
1832
+ continue
1833
+ try:
1834
+ chunks.append(path.read_text())
1835
+ except UnicodeDecodeError:
1836
+ continue
1837
+ return "\n".join(chunks)
1838
+
1839
+
1840
+ def evidence_prune_candidates(repo, root="docs/generated", older_than_days=14):
1841
+ evidence_root = (repo / root).resolve()
1842
+ if not evidence_root.exists():
1843
+ return []
1844
+ try:
1845
+ evidence_root.relative_to(repo.resolve())
1846
+ except ValueError as error:
1847
+ raise ValueError(f"Evidence root must be inside repo: {root}") from error
1848
+
1849
+ now = time.time()
1850
+ max_age_seconds = older_than_days * 24 * 60 * 60
1851
+ docs_text = docs_text_for_reference_scan(repo)
1852
+ candidates = []
1853
+ for path in sorted(evidence_root.rglob("*")):
1854
+ if not path.is_file():
1855
+ continue
1856
+ relative_path = str(path.relative_to(repo))
1857
+ try:
1858
+ content = path.read_text()
1859
+ except UnicodeDecodeError:
1860
+ content = ""
1861
+ if is_managed_text(content):
1862
+ continue
1863
+ age_seconds = now - path.stat().st_mtime
1864
+ if age_seconds < max_age_seconds:
1865
+ continue
1866
+ if relative_path in docs_text or path.name in docs_text:
1867
+ continue
1868
+ candidates.append(
1869
+ {
1870
+ "path": relative_path,
1871
+ "age_days": round(age_seconds / (24 * 60 * 60), 1),
1872
+ "reason": (
1873
+ f"unreferenced file under {root} older than {older_than_days} days "
1874
+ "and not a managed starter"
1875
+ ),
1876
+ }
1877
+ )
1878
+ return candidates
1879
+
1880
+
1881
+ def analyze_repo(repo):
1882
+ files = list_repo_files(repo)
1883
+ languages = detect_languages(files)
1884
+ frameworks = detect_frameworks(repo)
1885
+ package_managers = detect_package_managers(repo)
1886
+ has_frontend = any(name in frameworks for name in ["Next.js", "React", "Vue", "Svelte", "Vite"]) or any(
1887
+ file.endswith((".tsx", ".jsx", ".css", ".scss")) for file in files
1888
+ )
1889
+ existing_managed = detect_existing_managed_files(repo)
1890
+ existing_harness = [
1891
+ file for file in ["AGENTS.md", "ARCHITECTURE.md", "docs/PLANS.md", "docs/SECURITY.md"] if (repo / file).exists()
1892
+ ]
1893
+ missing_exec_plan_state = [
1894
+ path
1895
+ for path in [
1896
+ "docs/exec-plans/active/README.md",
1897
+ "docs/exec-plans/active/_template.md",
1898
+ "docs/exec-plans/completed/README.md",
1899
+ ]
1900
+ if not (repo / path).exists()
1901
+ ]
1902
+ missing_sops = [
1903
+ path
1904
+ for path in [
1905
+ "docs/sops/layered-domain-architecture-setup.md",
1906
+ "docs/sops/encode-unseen-knowledge.md",
1907
+ "docs/sops/local-observability-feedback-loop.md",
1908
+ "docs/sops/chrome-devtools-ui-validation-loop.md",
1909
+ "docs/sops/evidence-first-eval-loop.md",
1910
+ ]
1911
+ if not (repo / path).exists()
1912
+ ]
1913
+ durable_knowledge_targets = [
1914
+ "ARCHITECTURE.md",
1915
+ "docs/product-specs/",
1916
+ "docs/design-docs/",
1917
+ "docs/RELIABILITY.md",
1918
+ "docs/SECURITY.md",
1919
+ "docs/references/",
1920
+ ]
1921
+
1922
+ inferred_answers = {
1923
+ "project_name": repo.name,
1924
+ "languages": languages,
1925
+ "frameworks": frameworks,
1926
+ "package_managers": package_managers,
1927
+ "frontend_scope": (
1928
+ "A frontend surface likely exists."
1929
+ if has_frontend
1930
+ else "No obvious frontend surface detected from the repository."
1931
+ ),
1932
+ }
1933
+
1934
+ human_confirmations = []
1935
+ for question in QUESTION_CATALOG:
1936
+ if question["id"] == "frontend_stack_notes" and not has_frontend:
1937
+ continue
1938
+ human_confirmations.append(question)
1939
+
1940
+ analysis = {
1941
+ "project_name": repo.name,
1942
+ "repo_path": str(repo.resolve()),
1943
+ "languages": languages,
1944
+ "frameworks": frameworks,
1945
+ "package_managers": package_managers,
1946
+ "has_frontend": has_frontend,
1947
+ "inferred_answers": inferred_answers,
1948
+ "existing_harness_files": existing_harness,
1949
+ "existing_managed_files": existing_managed,
1950
+ "missing_exec_plan_state": missing_exec_plan_state,
1951
+ "missing_sops": missing_sops,
1952
+ "durable_knowledge_targets": durable_knowledge_targets,
1953
+ "human_confirmations": human_confirmations,
1954
+ "harness_state": "existing" if existing_harness or existing_managed else "new",
1955
+ "recommended_action": "init",
1956
+ "notes": [
1957
+ "Ask the human only the confirmations that the repository cannot answer safely.",
1958
+ "If unmanaged harness files already exist, preserve them unless the human explicitly requests replacement.",
1959
+ "Create execution-plan state before expecting agents to keep multi-step work synchronized.",
1960
+ "Use SOPs to turn recurring architecture, UI, observability, and knowledge-capture work into mechanical loops.",
1961
+ "Write durable facts into permanent docs instead of leaving them trapped inside plans or chat history.",
1962
+ ],
1963
+ }
1964
+ return analysis
1965
+
1966
+
1967
+ def load_json(path):
1968
+ return json.loads(Path(path).read_text())
1969
+
1970
+
1971
+ def write_json(path, payload):
1972
+ output = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
1973
+ if path:
1974
+ target = Path(path)
1975
+ ensure_parent(target)
1976
+ target.write_text(output)
1977
+ else:
1978
+ print(output, end="")
1979
+
1980
+
1981
+ def read_text_arg(value=None, file_path=None, label="value"):
1982
+ if value and file_path:
1983
+ raise ValueError(f"Use either --{label} or --{label}-file, not both")
1984
+ if file_path:
1985
+ return Path(file_path).read_text().strip()
1986
+ return value
1987
+
1988
+
1989
+ def command_analyze(args):
1990
+ repo = Path(args.repo).resolve()
1991
+ analysis = analyze_repo(repo)
1992
+ write_json(args.output, analysis)
1993
+
1994
+
1995
+ def command_sample_answers(args):
1996
+ analysis = load_json(args.analysis)
1997
+ payload = make_default_answers(analysis)
1998
+ write_json(args.output, payload)
1999
+
2000
+
2001
+ def command_init(args):
2002
+ repo = Path(args.repo).resolve()
2003
+ analysis = analyze_repo(repo)
2004
+ answers = load_json(args.answers)
2005
+ has_harness = bool(analysis["existing_harness_files"] or analysis["existing_managed_files"])
2006
+ effective_refresh = has_harness or args.force
2007
+ written, skipped, created, refreshed = write_scaffold(
2008
+ repo,
2009
+ analysis,
2010
+ answers,
2011
+ refresh_managed=effective_refresh,
2012
+ force=args.force,
2013
+ )
2014
+ result = {
2015
+ "repo": str(repo),
2016
+ "written": written,
2017
+ "created": created,
2018
+ "refreshed": refreshed,
2019
+ "skipped": skipped,
2020
+ "mode": "init",
2021
+ "operation": "reconciled" if has_harness else "created",
2022
+ "refresh_managed": effective_refresh,
2023
+ "force": args.force,
2024
+ }
2025
+ write_json(args.output, result)
2026
+
2027
+
2028
+ def command_plan_start(args):
2029
+ repo = Path(args.repo).resolve()
2030
+ plan_path = create_plan(repo, args.slug, args.goal)
2031
+ result = {"repo": str(repo), "plan": str(plan_path), "status": "created"}
2032
+ write_json(args.output, result)
2033
+
2034
+
2035
+ def command_knowledge_log(args):
2036
+ repo = Path(args.repo).resolve()
2037
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
2038
+ fact = read_text_arg(args.fact, args.fact_file, "fact")
2039
+ if not fact:
2040
+ raise ValueError("Provide --fact or --fact-file")
2041
+ item, item_id = append_knowledge_item(plan_path, fact, args.destination)
2042
+ result = {"repo": str(repo), "plan": str(plan_path), "id": item_id, "logged": item}
2043
+ write_json(args.output, result)
2044
+
2045
+
2046
+ def command_defect_log(args):
2047
+ repo = Path(args.repo).resolve()
2048
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
2049
+ summary = read_text_arg(args.summary, args.summary_file, "summary")
2050
+ evidence = read_text_arg(args.evidence, args.evidence_file, "evidence")
2051
+ if not summary:
2052
+ raise ValueError("Provide --summary or --summary-file")
2053
+ item, item_id = append_defect_item(plan_path, args.severity, summary, evidence=evidence)
2054
+ result = {"repo": str(repo), "plan": str(plan_path), "id": item_id, "logged": item, "status": "fail"}
2055
+ write_json(args.output, result)
2056
+ raise SystemExit(1)
2057
+
2058
+
2059
+ def command_defect_resolve(args):
2060
+ repo = Path(args.repo).resolve()
2061
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
2062
+ fix_evidence = read_text_arg(args.fix_evidence, args.fix_evidence_file, "fix-evidence")
2063
+ mark_defect_resolved(plan_path, args.id, fix_evidence)
2064
+ result = {
2065
+ "repo": str(repo),
2066
+ "plan": str(plan_path),
2067
+ "id": args.id,
2068
+ "status": "resolved",
2069
+ "fix_evidence": fix_evidence,
2070
+ }
2071
+ write_json(args.output, result)
2072
+
2073
+
2074
+ def command_plan_close(args):
2075
+ repo = Path(args.repo).resolve()
2076
+ destination, unresolved = close_plan(repo, args.plan, args.summary, args.force)
2077
+ result = {
2078
+ "repo": str(repo),
2079
+ "closed_plan": str(destination),
2080
+ "unresolved_items_forced": unresolved,
2081
+ "status": "closed",
2082
+ }
2083
+ write_json(args.output, result)
2084
+
2085
+
2086
+ def score_arg(args, name):
2087
+ value = getattr(args, name)
2088
+ if value < 0 or value > 10:
2089
+ raise ValueError(f"{name.replace('_', '-')} must be between 0 and 10")
2090
+ return float(value)
2091
+
2092
+
2093
+ def command_quality_score(args):
2094
+ repo = Path(args.repo).resolve()
2095
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
2096
+ scores = {
2097
+ "product_correctness": score_arg(args, "product_correctness"),
2098
+ "ux_operator_clarity": score_arg(args, "ux_operator_clarity"),
2099
+ "architecture_maintainability": score_arg(args, "architecture_maintainability"),
2100
+ "reliability_observability": score_arg(args, "reliability_observability"),
2101
+ "security_data_handling": score_arg(args, "security_data_handling"),
2102
+ }
2103
+ notes = {
2104
+ "product_correctness": args.product_note,
2105
+ "ux_operator_clarity": args.ux_note,
2106
+ "architecture_maintainability": args.architecture_note,
2107
+ "reliability_observability": args.reliability_note,
2108
+ "security_data_handling": args.security_note,
2109
+ }
2110
+ missing_notes = missing_quality_notes(notes)
2111
+ if missing_notes and not args.allow_empty_notes:
2112
+ result = {
2113
+ "status": "fail",
2114
+ "repo": str(repo),
2115
+ "plan": str(plan_path),
2116
+ "reason": "missing-quality-notes",
2117
+ "message": "quality-score requires evidence notes for every dimension.",
2118
+ "missing_notes": missing_notes,
2119
+ }
2120
+ write_json(args.output, result)
2121
+ raise SystemExit(1)
2122
+ result = update_quality_gate(plan_path, scores, notes, args.minimum)
2123
+ result.update({"repo": str(repo), "plan": str(plan_path)})
2124
+ write_json(args.output, result)
2125
+ if result["status"] != "pass":
2126
+ raise SystemExit(1)
2127
+
2128
+
2129
+ def command_phase_set(args):
2130
+ repo = Path(args.repo).resolve()
2131
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
2132
+ result = update_phase_continuity(
2133
+ plan_path,
2134
+ args.mode,
2135
+ args.workstream,
2136
+ args.current_phase,
2137
+ args.next_phase,
2138
+ args.continuation,
2139
+ args.next_action,
2140
+ args.closure_reason,
2141
+ args.resume_notes,
2142
+ )
2143
+ result.update({"repo": str(repo), "plan": str(plan_path)})
2144
+ write_json(args.output, result)
2145
+
2146
+
2147
+ def command_workstream_upsert(args):
2148
+ repo = Path(args.repo).resolve()
2149
+ target = append_workstream_entry(
2150
+ repo,
2151
+ args.id,
2152
+ args.status,
2153
+ args.current_plan,
2154
+ args.last_completed_plan,
2155
+ args.next_action,
2156
+ args.goal,
2157
+ args.resume_notes,
2158
+ )
2159
+ result = {"repo": str(repo), "workstreams": str(target), "id": args.id, "status": "updated"}
2160
+ write_json(args.output, result)
2161
+
2162
+
2163
+ def command_knowledge_mark_written(args):
2164
+ repo = Path(args.repo).resolve()
2165
+ plan_path, _ = plan_path_from_arg(repo, args.plan)
2166
+ fact = read_text_arg(args.fact, args.fact_file, "fact")
2167
+ evidence = read_text_arg(args.evidence, args.evidence_file, "evidence")
2168
+ mark_single_knowledge_item_written(
2169
+ repo,
2170
+ plan_path,
2171
+ fact,
2172
+ args.destination,
2173
+ append=args.append,
2174
+ knowledge_id=args.id,
2175
+ evidence=evidence,
2176
+ )
2177
+ result = {
2178
+ "repo": str(repo),
2179
+ "plan": str(plan_path),
2180
+ "marked_written": args.id or fact,
2181
+ "destination": args.destination,
2182
+ "evidence": evidence,
2183
+ }
2184
+ write_json(args.output, result)
2185
+
2186
+
2187
+ def command_check(args):
2188
+ repo = Path(args.repo).resolve()
2189
+ result = check_harness(repo)
2190
+ write_json(args.output, result)
2191
+ if result["status"] != "pass":
2192
+ raise SystemExit(1)
2193
+
2194
+
2195
+ def command_evidence_prune(args):
2196
+ repo = Path(args.repo).resolve()
2197
+ candidates = evidence_prune_candidates(
2198
+ repo,
2199
+ root=args.root,
2200
+ older_than_days=args.older_than_days,
2201
+ )
2202
+ removed = []
2203
+ if args.apply:
2204
+ for candidate in candidates:
2205
+ path = repo / candidate["path"]
2206
+ if path.exists() and path.is_file():
2207
+ path.unlink()
2208
+ removed.append(candidate["path"])
2209
+ result = {
2210
+ "repo": str(repo),
2211
+ "root": args.root,
2212
+ "older_than_days": args.older_than_days,
2213
+ "mode": "apply" if args.apply else "dry-run",
2214
+ "candidate_count": len(candidates),
2215
+ "candidates": candidates,
2216
+ "removed": removed,
2217
+ }
2218
+ write_json(args.output, result)
2219
+
2220
+
2221
+ def build_parser():
2222
+ parser = argparse.ArgumentParser(description="Manage the harness repo scaffold.")
2223
+ subparsers = parser.add_subparsers(dest="command", required=True)
2224
+
2225
+ analyze = subparsers.add_parser("analyze")
2226
+ analyze.add_argument("--repo", required=True)
2227
+ analyze.add_argument("--output")
2228
+ analyze.set_defaults(func=command_analyze)
2229
+
2230
+ sample_answers = subparsers.add_parser("sample-answers")
2231
+ sample_answers.add_argument("--analysis", required=True)
2232
+ sample_answers.add_argument("--output")
2233
+ sample_answers.set_defaults(func=command_sample_answers)
2234
+
2235
+ init = subparsers.add_parser("init")
2236
+ init.add_argument("--repo", required=True)
2237
+ init.add_argument("--answers", required=True)
2238
+ init.add_argument("--output")
2239
+ init.add_argument("--force", action="store_true")
2240
+ init.set_defaults(func=command_init)
2241
+
2242
+ plan_start = subparsers.add_parser("plan-start")
2243
+ plan_start.add_argument("--repo", required=True)
2244
+ plan_start.add_argument("--slug", required=True)
2245
+ plan_start.add_argument("--goal", required=True)
2246
+ plan_start.add_argument("--output")
2247
+ plan_start.set_defaults(func=command_plan_start)
2248
+
2249
+ knowledge_log = subparsers.add_parser("knowledge-log")
2250
+ knowledge_log.add_argument("--repo", required=True)
2251
+ knowledge_log.add_argument("--plan", required=True)
2252
+ knowledge_log.add_argument("--fact")
2253
+ knowledge_log.add_argument("--fact-file")
2254
+ knowledge_log.add_argument("--destination", required=True)
2255
+ knowledge_log.add_argument("--output")
2256
+ knowledge_log.set_defaults(func=command_knowledge_log)
2257
+
2258
+ defect_log = subparsers.add_parser("defect-log")
2259
+ defect_log.add_argument("--repo", required=True)
2260
+ defect_log.add_argument("--plan", required=True)
2261
+ defect_log.add_argument("--severity", choices=["P0", "P1", "P2", "P3"], required=True)
2262
+ defect_log.add_argument("--summary")
2263
+ defect_log.add_argument("--summary-file")
2264
+ defect_log.add_argument("--evidence")
2265
+ defect_log.add_argument("--evidence-file")
2266
+ defect_log.add_argument("--output")
2267
+ defect_log.set_defaults(func=command_defect_log)
2268
+
2269
+ defect_resolve = subparsers.add_parser("defect-resolve")
2270
+ defect_resolve.add_argument("--repo", required=True)
2271
+ defect_resolve.add_argument("--plan", required=True)
2272
+ defect_resolve.add_argument("--id", required=True)
2273
+ defect_resolve.add_argument("--fix-evidence")
2274
+ defect_resolve.add_argument("--fix-evidence-file")
2275
+ defect_resolve.add_argument("--output")
2276
+ defect_resolve.set_defaults(func=command_defect_resolve)
2277
+
2278
+ knowledge_mark_written = subparsers.add_parser("knowledge-mark-written")
2279
+ knowledge_mark_written.add_argument("--repo", required=True)
2280
+ knowledge_mark_written.add_argument("--plan", required=True)
2281
+ knowledge_mark_written.add_argument("--id")
2282
+ knowledge_mark_written.add_argument("--fact")
2283
+ knowledge_mark_written.add_argument("--fact-file")
2284
+ knowledge_mark_written.add_argument("--destination")
2285
+ knowledge_mark_written.add_argument("--evidence")
2286
+ knowledge_mark_written.add_argument("--evidence-file")
2287
+ knowledge_mark_written.add_argument("--append", action="store_true")
2288
+ knowledge_mark_written.add_argument("--output")
2289
+ knowledge_mark_written.set_defaults(func=command_knowledge_mark_written)
2290
+
2291
+ plan_close = subparsers.add_parser("plan-close")
2292
+ plan_close.add_argument("--repo", required=True)
2293
+ plan_close.add_argument("--plan", required=True)
2294
+ plan_close.add_argument("--summary", required=True)
2295
+ plan_close.add_argument("--force", action="store_true")
2296
+ plan_close.add_argument("--output")
2297
+ plan_close.set_defaults(func=command_plan_close)
2298
+
2299
+ quality_score = subparsers.add_parser("quality-score")
2300
+ quality_score.add_argument("--repo", required=True)
2301
+ quality_score.add_argument("--plan", required=True)
2302
+ quality_score.add_argument("--minimum", type=float, default=8.0)
2303
+ quality_score.add_argument("--product-correctness", type=float, required=True)
2304
+ quality_score.add_argument("--ux-operator-clarity", type=float, required=True)
2305
+ quality_score.add_argument("--architecture-maintainability", type=float, required=True)
2306
+ quality_score.add_argument("--reliability-observability", type=float, required=True)
2307
+ quality_score.add_argument("--security-data-handling", type=float, required=True)
2308
+ quality_score.add_argument("--product-note", default="")
2309
+ quality_score.add_argument("--ux-note", default="")
2310
+ quality_score.add_argument("--architecture-note", default="")
2311
+ quality_score.add_argument("--reliability-note", default="")
2312
+ quality_score.add_argument("--security-note", default="")
2313
+ quality_score.add_argument("--allow-empty-notes", action="store_true")
2314
+ quality_score.add_argument("--output")
2315
+ quality_score.set_defaults(func=command_quality_score)
2316
+
2317
+ phase_set = subparsers.add_parser("phase-set")
2318
+ phase_set.add_argument("--repo", required=True)
2319
+ phase_set.add_argument("--plan", required=True)
2320
+ phase_set.add_argument(
2321
+ "--mode",
2322
+ choices=["single-phase", "multi-phase", "paused", "completed", "stopped"],
2323
+ required=True,
2324
+ )
2325
+ phase_set.add_argument("--workstream")
2326
+ phase_set.add_argument("--current-phase")
2327
+ phase_set.add_argument("--next-phase", default="none")
2328
+ phase_set.add_argument("--continuation", default="none")
2329
+ phase_set.add_argument("--next-action", default="none")
2330
+ phase_set.add_argument("--closure-reason", default="none")
2331
+ phase_set.add_argument("--resume-notes", default="none")
2332
+ phase_set.add_argument("--output")
2333
+ phase_set.set_defaults(func=command_phase_set)
2334
+
2335
+ workstream_upsert = subparsers.add_parser("workstream-upsert")
2336
+ workstream_upsert.add_argument("--repo", required=True)
2337
+ workstream_upsert.add_argument("--id", required=True)
2338
+ workstream_upsert.add_argument(
2339
+ "--status",
2340
+ choices=["active", "paused", "completed", "stopped"],
2341
+ required=True,
2342
+ )
2343
+ workstream_upsert.add_argument("--current-plan", default="none")
2344
+ workstream_upsert.add_argument("--last-completed-plan", default="none")
2345
+ workstream_upsert.add_argument("--next-action", required=True)
2346
+ workstream_upsert.add_argument("--goal", default="")
2347
+ workstream_upsert.add_argument("--resume-notes", default="")
2348
+ workstream_upsert.add_argument("--output")
2349
+ workstream_upsert.set_defaults(func=command_workstream_upsert)
2350
+
2351
+ check = subparsers.add_parser("check")
2352
+ check.add_argument("--repo", required=True)
2353
+ check.add_argument("--output")
2354
+ check.set_defaults(func=command_check)
2355
+
2356
+ evidence_prune = subparsers.add_parser("evidence-prune")
2357
+ evidence_prune.add_argument("--repo", required=True)
2358
+ evidence_prune.add_argument("--root", default="docs/generated")
2359
+ evidence_prune.add_argument("--older-than-days", type=int, default=14)
2360
+ evidence_prune.add_argument("--apply", action="store_true")
2361
+ evidence_prune.add_argument("--output")
2362
+ evidence_prune.set_defaults(func=command_evidence_prune)
2363
+
2364
+ return parser
2365
+
2366
+
2367
+ def main():
2368
+ parser = build_parser()
2369
+ args = parser.parse_args()
2370
+ args.func(args)
2371
+
2372
+
2373
+ if __name__ == "__main__":
2374
+ main()