@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.
- package/README.md +262 -0
- package/bin/install.js +154 -0
- package/package.json +31 -0
- package/skills/harness-engine/SKILL.md +82 -0
- package/skills/harness-engine/agents/openai.yaml +4 -0
- package/skills/harness-engine/assets/repo-template/.keep +1 -0
- package/skills/harness-engine/assets/sops/.keep +1 -0
- package/skills/harness-engine/evals/cases.json +50 -0
- package/skills/harness-engine/evals/run_evals.py +1188 -0
- package/skills/harness-engine/references/evaluation-loop.md +24 -0
- package/skills/harness-engine/references/evidence-first-evals.md +180 -0
- package/skills/harness-engine/references/exec-plans.md +51 -0
- package/skills/harness-engine/references/file-map.md +17 -0
- package/skills/harness-engine/references/knowledge-capture.md +35 -0
- package/skills/harness-engine/references/question-catalog.md +29 -0
- package/skills/harness-engine/references/sop-index.md +12 -0
- package/skills/harness-engine/references/template-policy.md +13 -0
- package/skills/harness-engine/references/workflow.md +55 -0
- package/skills/harness-engine/scripts/manage_harness.py +2374 -0
|
@@ -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()
|