@hallucination-studio/harness-engine 1.0.0 → 1.0.1
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/LICENSE +21 -0
- package/README.md +185 -27
- package/bin/install.js +29 -17
- package/package.json +10 -4
- package/skills/harness-engine/SKILL.md +97 -0
- package/skills/harness-engine/agents/openai.yaml +4 -0
- package/skills/harness-engine/evals/cases.json +94 -0
- package/skills/harness-engine/evals/harness_engine_evals/__init__.py +1 -0
- package/skills/harness-engine/evals/harness_engine_evals/cases_frontend.py +211 -0
- package/skills/harness-engine/evals/harness_engine_evals/cases_lifecycle.py +1616 -0
- package/skills/harness-engine/evals/harness_engine_evals/helpers.py +155 -0
- package/skills/harness-engine/evals/harness_engine_evals/registry.py +55 -0
- package/skills/harness-engine/evals/harness_engine_evals/report.py +36 -0
- package/skills/harness-engine/evals/harness_engine_evals/runner.py +53 -0
- package/skills/harness-engine/evals/run_evals.py +14 -0
- package/skills/{harness-repo-bootstrap → harness-engine}/references/evaluation-loop.md +8 -2
- package/skills/harness-engine/references/evidence-first-evals.md +187 -0
- package/skills/harness-engine/references/exec-plans.md +59 -0
- package/skills/{harness-repo-bootstrap → harness-engine}/references/file-map.md +3 -3
- package/skills/{harness-repo-bootstrap → harness-engine}/references/knowledge-capture.md +2 -2
- package/skills/{harness-repo-bootstrap → harness-engine}/references/sop-index.md +3 -0
- package/skills/harness-engine/references/template-policy.md +17 -0
- package/skills/harness-engine/references/workflow.md +62 -0
- package/skills/harness-engine/scripts/harness_engine/__init__.py +1 -0
- package/skills/harness-engine/scripts/harness_engine/analysis.py +240 -0
- package/skills/harness-engine/scripts/harness_engine/checks.py +287 -0
- package/skills/harness-engine/scripts/harness_engine/cli.py +656 -0
- package/skills/harness-engine/scripts/harness_engine/common.py +977 -0
- package/skills/harness-engine/scripts/harness_engine/continuation.py +520 -0
- package/skills/harness-engine/scripts/harness_engine/git_ops.py +88 -0
- package/skills/harness-engine/scripts/harness_engine/knowledge.py +329 -0
- package/skills/harness-engine/scripts/harness_engine/plans.py +630 -0
- package/skills/harness-engine/scripts/harness_engine/templates.py +124 -0
- package/skills/harness-engine/scripts/manage_harness.py +14 -0
- package/skills/harness-repo-bootstrap/SKILL.md +0 -68
- package/skills/harness-repo-bootstrap/agents/openai.yaml +0 -4
- package/skills/harness-repo-bootstrap/evals/cases.json +0 -18
- package/skills/harness-repo-bootstrap/evals/run_evals.py +0 -337
- package/skills/harness-repo-bootstrap/references/exec-plans.md +0 -39
- package/skills/harness-repo-bootstrap/references/template-policy.md +0 -12
- package/skills/harness-repo-bootstrap/references/workflow.md +0 -47
- package/skills/harness-repo-bootstrap/scripts/manage_harness.py +0 -1181
- /package/skills/{harness-repo-bootstrap → harness-engine}/assets/repo-template/.keep +0 -0
- /package/skills/{harness-repo-bootstrap → harness-engine}/assets/sops/.keep +0 -0
- /package/skills/{harness-repo-bootstrap → harness-engine}/references/question-catalog.md +0 -0
|
@@ -1,1181 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import hashlib
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
import re
|
|
8
|
-
from datetime import UTC, datetime
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
MANAGED_MARKER = "<!-- harness-repo-bootstrap:managed -->"
|
|
12
|
-
PLAN_TEMPLATE = """# Execution Plan: {title}
|
|
13
|
-
|
|
14
|
-
## Goal
|
|
15
|
-
|
|
16
|
-
{goal}
|
|
17
|
-
|
|
18
|
-
## Scope
|
|
19
|
-
|
|
20
|
-
- Define in-scope work.
|
|
21
|
-
- Define out-of-scope work.
|
|
22
|
-
|
|
23
|
-
## Constraints
|
|
24
|
-
|
|
25
|
-
- Add relevant product, architecture, reliability, security, or delivery constraints.
|
|
26
|
-
|
|
27
|
-
## Steps
|
|
28
|
-
|
|
29
|
-
1. Add the first concrete step.
|
|
30
|
-
2. Add the next concrete step.
|
|
31
|
-
|
|
32
|
-
## Validation
|
|
33
|
-
|
|
34
|
-
- Describe how the work will be verified.
|
|
35
|
-
|
|
36
|
-
## Durable Knowledge To Capture
|
|
37
|
-
|
|
38
|
-
{knowledge_section}
|
|
39
|
-
|
|
40
|
-
## Completion Notes
|
|
41
|
-
|
|
42
|
-
Pending.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
ROOT_FILES = {
|
|
46
|
-
"AGENTS.md": """{marker}
|
|
47
|
-
# AGENTS
|
|
48
|
-
|
|
49
|
-
Read this file first, then follow the linked docs.
|
|
50
|
-
|
|
51
|
-
## Routing
|
|
52
|
-
|
|
53
|
-
- Read `ARCHITECTURE.md` before changing boundaries, data flow, or integrations.
|
|
54
|
-
- Read `docs/PLANS.md` before starting multi-step execution work.
|
|
55
|
-
- Read `docs/exec-plans/active/` before resuming in-flight work, and create a plan there for new multi-step work.
|
|
56
|
-
- Read `docs/QUALITY_SCORE.md` before evaluating tradeoffs or readiness.
|
|
57
|
-
- Read `docs/RELIABILITY.md` for runtime validation and failure handling.
|
|
58
|
-
- Read `docs/SECURITY.md` before touching auth, secrets, or sensitive data.
|
|
59
|
-
- Read `docs/FRONTEND.md` for UI or terminal interface changes.
|
|
60
|
-
- Read the matching file in `docs/sops/` before architecture changes, UI validation, observability work, or knowledge capture.
|
|
61
|
-
|
|
62
|
-
## Repository Focus
|
|
63
|
-
|
|
64
|
-
- Project: {project_name}
|
|
65
|
-
- Domain: {product_domain}
|
|
66
|
-
- Primary outcome: {project_summary}
|
|
67
|
-
- Main users: {primary_users}
|
|
68
|
-
|
|
69
|
-
## Operating Rules
|
|
70
|
-
|
|
71
|
-
- Keep durable decisions in repo docs, not only in chat.
|
|
72
|
-
- Keep active plans in `docs/exec-plans/active/`.
|
|
73
|
-
- Move completed plans to `docs/exec-plans/completed/`.
|
|
74
|
-
- Update plans during the work, not only at the end.
|
|
75
|
-
- Encode durable facts learned during execution into permanent docs before closing the task.
|
|
76
|
-
- Before handoff, run the local harness check: `python3 .codex/skills/harness-repo-bootstrap/scripts/manage_harness.py check --repo .`.
|
|
77
|
-
- Keep generated artifacts in `docs/generated/`.
|
|
78
|
-
- Keep external references in `docs/references/`.
|
|
79
|
-
""",
|
|
80
|
-
"ARCHITECTURE.md": """{marker}
|
|
81
|
-
# Architecture
|
|
82
|
-
|
|
83
|
-
## System Summary
|
|
84
|
-
|
|
85
|
-
{project_summary}
|
|
86
|
-
|
|
87
|
-
## Domain Boundaries
|
|
88
|
-
|
|
89
|
-
- Product domain: {product_domain}
|
|
90
|
-
- Primary users: {primary_users}
|
|
91
|
-
- Deployment targets: {deployment_targets}
|
|
92
|
-
|
|
93
|
-
## Repository Shape
|
|
94
|
-
|
|
95
|
-
- Detected languages: {languages}
|
|
96
|
-
- Detected package managers: {package_managers}
|
|
97
|
-
- Detected frameworks: {frameworks}
|
|
98
|
-
|
|
99
|
-
## Reliability Architecture
|
|
100
|
-
|
|
101
|
-
{reliability_targets}
|
|
102
|
-
|
|
103
|
-
## Security Architecture
|
|
104
|
-
|
|
105
|
-
{security_constraints}
|
|
106
|
-
|
|
107
|
-
## Open Questions
|
|
108
|
-
|
|
109
|
-
- Document major runtime boundaries, shared libraries, and integration seams here as the codebase grows.
|
|
110
|
-
""",
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
DOC_FILES = {
|
|
114
|
-
"docs/DESIGN.md": """{marker}
|
|
115
|
-
# Design
|
|
116
|
-
|
|
117
|
-
## Product Experience Bar
|
|
118
|
-
|
|
119
|
-
{frontend_stack_notes}
|
|
120
|
-
|
|
121
|
-
## Review Heuristics
|
|
122
|
-
|
|
123
|
-
- Prefer intentional interaction patterns over generic defaults.
|
|
124
|
-
- Keep visual and UX rationale durable in `docs/design-docs/`.
|
|
125
|
-
- Validate meaningful UI work in a real browser before closing it out.
|
|
126
|
-
""",
|
|
127
|
-
"docs/FRONTEND.md": """{marker}
|
|
128
|
-
# Frontend
|
|
129
|
-
|
|
130
|
-
## Scope
|
|
131
|
-
|
|
132
|
-
{frontend_scope}
|
|
133
|
-
|
|
134
|
-
## Stack Notes
|
|
135
|
-
|
|
136
|
-
{frontend_stack_notes}
|
|
137
|
-
|
|
138
|
-
## Validation Loop
|
|
139
|
-
|
|
140
|
-
{frontend_validation_loop}
|
|
141
|
-
""",
|
|
142
|
-
"docs/PLANS.md": """{marker}
|
|
143
|
-
# Plans
|
|
144
|
-
|
|
145
|
-
## Plan Lifecycle
|
|
146
|
-
|
|
147
|
-
- Put active execution plans in `docs/exec-plans/active/`.
|
|
148
|
-
- Move completed plans to `docs/exec-plans/completed/`.
|
|
149
|
-
- Record cross-cutting follow-up work in `docs/exec-plans/tech-debt-tracker.md`.
|
|
150
|
-
|
|
151
|
-
## Authoring Rules
|
|
152
|
-
|
|
153
|
-
- Keep plans concrete, testable, and scoped.
|
|
154
|
-
- Update plans during the work, not after the fact.
|
|
155
|
-
- Link to specs, decisions, and validation artifacts when they exist.
|
|
156
|
-
- Include a section for durable knowledge that must be written back into permanent docs.
|
|
157
|
-
- Do not treat plans as the final home for product, architecture, or policy knowledge.
|
|
158
|
-
""",
|
|
159
|
-
"docs/PRODUCT_SENSE.md": """{marker}
|
|
160
|
-
# Product Sense
|
|
161
|
-
|
|
162
|
-
## Product Summary
|
|
163
|
-
|
|
164
|
-
{project_summary}
|
|
165
|
-
|
|
166
|
-
## Users
|
|
167
|
-
|
|
168
|
-
{primary_users}
|
|
169
|
-
|
|
170
|
-
## Decision Rules
|
|
171
|
-
|
|
172
|
-
- Optimize for the main user outcome before edge polish.
|
|
173
|
-
- Make tradeoffs explicit when speed, quality, and scope conflict.
|
|
174
|
-
- Capture durable product decisions in `docs/product-specs/`.
|
|
175
|
-
""",
|
|
176
|
-
"docs/QUALITY_SCORE.md": """{marker}
|
|
177
|
-
# Quality Score
|
|
178
|
-
|
|
179
|
-
## Priority Areas
|
|
180
|
-
|
|
181
|
-
{quality_focus}
|
|
182
|
-
|
|
183
|
-
## Scoring Dimensions
|
|
184
|
-
|
|
185
|
-
- Product correctness
|
|
186
|
-
- UX and operator clarity
|
|
187
|
-
- Architecture and maintainability
|
|
188
|
-
- Reliability and observability
|
|
189
|
-
- Security and data handling
|
|
190
|
-
|
|
191
|
-
## Usage
|
|
192
|
-
|
|
193
|
-
- Score changes by affected domain and layer.
|
|
194
|
-
- Document recurring weak spots and improvement themes here.
|
|
195
|
-
""",
|
|
196
|
-
"docs/RELIABILITY.md": """{marker}
|
|
197
|
-
# Reliability
|
|
198
|
-
|
|
199
|
-
## Reliability Targets
|
|
200
|
-
|
|
201
|
-
{reliability_targets}
|
|
202
|
-
|
|
203
|
-
## Runtime Validation
|
|
204
|
-
|
|
205
|
-
- Define the smallest useful local validation loop.
|
|
206
|
-
- Document required health checks, logs, and dashboards.
|
|
207
|
-
- Capture recurring incidents or near misses in repo docs.
|
|
208
|
-
""",
|
|
209
|
-
"docs/SECURITY.md": """{marker}
|
|
210
|
-
# Security
|
|
211
|
-
|
|
212
|
-
## Security Constraints
|
|
213
|
-
|
|
214
|
-
{security_constraints}
|
|
215
|
-
|
|
216
|
-
## Review Rules
|
|
217
|
-
|
|
218
|
-
- Review auth, authorization, secrets, and sensitive data changes explicitly.
|
|
219
|
-
- Prefer least privilege and traceable configuration.
|
|
220
|
-
- Record security-sensitive assumptions in durable docs.
|
|
221
|
-
""",
|
|
222
|
-
"docs/design-docs/index.md": """{marker}
|
|
223
|
-
# Design Docs Index
|
|
224
|
-
|
|
225
|
-
- Add one document per durable design decision.
|
|
226
|
-
- Link active design decisions from plans and specs.
|
|
227
|
-
""",
|
|
228
|
-
"docs/design-docs/core-beliefs.md": """{marker}
|
|
229
|
-
# Core Beliefs
|
|
230
|
-
|
|
231
|
-
- Keep the repository as the system of record.
|
|
232
|
-
- Prefer explicit policies over implied team memory.
|
|
233
|
-
- Prefer repeatable checks over remembered rules.
|
|
234
|
-
""",
|
|
235
|
-
"docs/exec-plans/tech-debt-tracker.md": """{marker}
|
|
236
|
-
# Tech Debt Tracker
|
|
237
|
-
|
|
238
|
-
Record follow-up work that should survive beyond a single execution plan.
|
|
239
|
-
""",
|
|
240
|
-
"docs/exec-plans/active/README.md": """{marker}
|
|
241
|
-
# Active Execution Plans
|
|
242
|
-
|
|
243
|
-
Create one markdown file per in-flight multi-step task.
|
|
244
|
-
|
|
245
|
-
Suggested filename:
|
|
246
|
-
|
|
247
|
-
`YYYY-MM-DD-short-task-name.md`
|
|
248
|
-
|
|
249
|
-
Minimum contents:
|
|
250
|
-
|
|
251
|
-
- goal
|
|
252
|
-
- scope
|
|
253
|
-
- constraints
|
|
254
|
-
- steps
|
|
255
|
-
- validation
|
|
256
|
-
- durable knowledge to capture
|
|
257
|
-
""",
|
|
258
|
-
"docs/exec-plans/active/_template.md": """{marker}
|
|
259
|
-
# Execution Plan: <title>
|
|
260
|
-
|
|
261
|
-
## Goal
|
|
262
|
-
|
|
263
|
-
Describe the intended outcome.
|
|
264
|
-
|
|
265
|
-
## Scope
|
|
266
|
-
|
|
267
|
-
Describe what is included and excluded.
|
|
268
|
-
|
|
269
|
-
## Constraints
|
|
270
|
-
|
|
271
|
-
List product, architecture, reliability, security, or delivery constraints.
|
|
272
|
-
|
|
273
|
-
## Steps
|
|
274
|
-
|
|
275
|
-
1. Add the first concrete step.
|
|
276
|
-
2. Add the next step.
|
|
277
|
-
|
|
278
|
-
## Validation
|
|
279
|
-
|
|
280
|
-
- Describe how the work will be verified.
|
|
281
|
-
|
|
282
|
-
## Durable Knowledge To Capture
|
|
283
|
-
|
|
284
|
-
- List facts that must be written back into permanent docs before completion.
|
|
285
|
-
|
|
286
|
-
## Completion Notes
|
|
287
|
-
|
|
288
|
-
Summarize outcomes, follow-ups, and doc updates.
|
|
289
|
-
""",
|
|
290
|
-
"docs/exec-plans/completed/README.md": """{marker}
|
|
291
|
-
# Completed Execution Plans
|
|
292
|
-
|
|
293
|
-
Move finished plans here after:
|
|
294
|
-
|
|
295
|
-
1. validation is complete
|
|
296
|
-
2. permanent docs have been updated
|
|
297
|
-
3. any remaining follow-ups are recorded in tech debt or new plans
|
|
298
|
-
""",
|
|
299
|
-
"docs/generated/db-schema.md": """{marker}
|
|
300
|
-
# Generated DB Schema
|
|
301
|
-
|
|
302
|
-
Place generated database or storage schema snapshots here when relevant.
|
|
303
|
-
""",
|
|
304
|
-
"docs/product-specs/index.md": """{marker}
|
|
305
|
-
# Product Specs Index
|
|
306
|
-
|
|
307
|
-
- Add one durable product spec per important workflow or product area.
|
|
308
|
-
- Link the active plan that created or changed each spec when useful.
|
|
309
|
-
""",
|
|
310
|
-
"docs/product-specs/new-user-onboarding.md": """{marker}
|
|
311
|
-
# New User Onboarding
|
|
312
|
-
|
|
313
|
-
## Outcome
|
|
314
|
-
|
|
315
|
-
Describe the desired first successful experience for a new user of {project_name}.
|
|
316
|
-
|
|
317
|
-
## Open Questions
|
|
318
|
-
|
|
319
|
-
- What must a new user understand before reaching value?
|
|
320
|
-
- Which steps are fragile or confusing today?
|
|
321
|
-
""",
|
|
322
|
-
"docs/references/design-system-reference-llms.txt": "Add model-friendly design system notes or links here.\n",
|
|
323
|
-
"docs/references/nixpacks-llms.txt": "Add model-friendly deployment or buildpack notes here.\n",
|
|
324
|
-
"docs/references/uv-llms.txt": "Add model-friendly Python tooling notes here.\n",
|
|
325
|
-
"docs/sops/layered-domain-architecture-setup.md": """{marker}
|
|
326
|
-
# SOP: Layered Domain Architecture Setup
|
|
327
|
-
|
|
328
|
-
1. Identify user-facing domains and bounded contexts.
|
|
329
|
-
2. Map code ownership and integration seams.
|
|
330
|
-
3. Record allowed dependency direction between layers.
|
|
331
|
-
4. Capture the result in `ARCHITECTURE.md` and the relevant design docs.
|
|
332
|
-
""",
|
|
333
|
-
"docs/sops/encode-unseen-knowledge.md": """{marker}
|
|
334
|
-
# SOP: Encode Unseen Knowledge
|
|
335
|
-
|
|
336
|
-
1. Notice repeated chat-only facts or tribal knowledge.
|
|
337
|
-
2. Decide the right durable home inside `docs/`.
|
|
338
|
-
3. Write the fact in concise, retrievable language.
|
|
339
|
-
4. Link it from the nearest routing doc if it will be reused often.
|
|
340
|
-
""",
|
|
341
|
-
"docs/sops/local-observability-feedback-loop.md": """{marker}
|
|
342
|
-
# SOP: Local Observability Feedback Loop
|
|
343
|
-
|
|
344
|
-
1. Run the narrowest local reproduction of the issue.
|
|
345
|
-
2. Capture logs, metrics, traces, or screenshots.
|
|
346
|
-
3. Tighten the validation loop until failures are easy to observe.
|
|
347
|
-
4. Record the durable validation path in `docs/RELIABILITY.md`.
|
|
348
|
-
""",
|
|
349
|
-
"docs/sops/chrome-devtools-ui-validation-loop.md": """{marker}
|
|
350
|
-
# SOP: Chrome DevTools UI Validation Loop
|
|
351
|
-
|
|
352
|
-
1. Open the relevant route in a browser.
|
|
353
|
-
2. Check layout, interaction, loading, error, and empty states.
|
|
354
|
-
3. Verify responsive behavior for the intended breakpoints.
|
|
355
|
-
4. Write reusable findings back to `docs/FRONTEND.md` or `docs/design-docs/`.
|
|
356
|
-
""",
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
QUESTION_CATALOG = [
|
|
360
|
-
{
|
|
361
|
-
"id": "project_summary",
|
|
362
|
-
"prompt": "What is the main user or business outcome this repository exists to deliver?",
|
|
363
|
-
"reason": "Needed for AGENTS, ARCHITECTURE, and product docs.",
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
"id": "primary_users",
|
|
367
|
-
"prompt": "Who are the primary users or operators of this repository?",
|
|
368
|
-
"reason": "Needed to make product and quality tradeoffs concrete.",
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
"id": "deployment_targets",
|
|
372
|
-
"prompt": "Where does this system run or get deployed?",
|
|
373
|
-
"reason": "Needed for architecture and reliability guidance.",
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
"id": "product_domain",
|
|
377
|
-
"prompt": "Which product domain best describes this repository?",
|
|
378
|
-
"reason": "Needed for quality scoring and policy language.",
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
"id": "reliability_targets",
|
|
382
|
-
"prompt": "Which uptime, recovery, or runtime validation expectations matter most?",
|
|
383
|
-
"reason": "Needed for reliability docs and validation loops.",
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
"id": "security_constraints",
|
|
387
|
-
"prompt": "Which security, compliance, auth, or sensitive-data constraints matter here?",
|
|
388
|
-
"reason": "Needed for security review guidance.",
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
"id": "frontend_stack_notes",
|
|
392
|
-
"prompt": "If there is a frontend, what experience bar, platforms, or UX constraints should the docs enforce?",
|
|
393
|
-
"reason": "Needed for design and frontend policies.",
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
"id": "quality_focus",
|
|
397
|
-
"prompt": "Which product areas or architectural layers deserve the strictest quality scoring?",
|
|
398
|
-
"reason": "Needed for QUALITY_SCORE.md.",
|
|
399
|
-
},
|
|
400
|
-
]
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
def detect_languages(files):
|
|
404
|
-
ext_map = {}
|
|
405
|
-
for file_path in files:
|
|
406
|
-
suffix = Path(file_path).suffix.lower()
|
|
407
|
-
if suffix:
|
|
408
|
-
ext_map[suffix] = ext_map.get(suffix, 0) + 1
|
|
409
|
-
mapping = {
|
|
410
|
-
".js": "JavaScript",
|
|
411
|
-
".jsx": "JavaScript",
|
|
412
|
-
".ts": "TypeScript",
|
|
413
|
-
".tsx": "TypeScript",
|
|
414
|
-
".sh": "Shell",
|
|
415
|
-
".bash": "Shell",
|
|
416
|
-
".zsh": "Shell",
|
|
417
|
-
".py": "Python",
|
|
418
|
-
".rb": "Ruby",
|
|
419
|
-
".go": "Go",
|
|
420
|
-
".rs": "Rust",
|
|
421
|
-
".java": "Java",
|
|
422
|
-
".kt": "Kotlin",
|
|
423
|
-
".swift": "Swift",
|
|
424
|
-
}
|
|
425
|
-
languages = []
|
|
426
|
-
for ext, language in mapping.items():
|
|
427
|
-
if ext in ext_map and language not in languages:
|
|
428
|
-
languages.append(language)
|
|
429
|
-
return languages
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
def read_json_if_exists(path):
|
|
433
|
-
if not path.exists():
|
|
434
|
-
return None
|
|
435
|
-
try:
|
|
436
|
-
return json.loads(path.read_text())
|
|
437
|
-
except json.JSONDecodeError:
|
|
438
|
-
return None
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
def detect_frameworks(repo):
|
|
442
|
-
frameworks = []
|
|
443
|
-
package_json = read_json_if_exists(repo / "package.json")
|
|
444
|
-
if package_json:
|
|
445
|
-
deps = {}
|
|
446
|
-
deps.update(package_json.get("dependencies", {}))
|
|
447
|
-
deps.update(package_json.get("devDependencies", {}))
|
|
448
|
-
dep_names = set(deps.keys())
|
|
449
|
-
known = {
|
|
450
|
-
"next": "Next.js",
|
|
451
|
-
"react": "React",
|
|
452
|
-
"vue": "Vue",
|
|
453
|
-
"svelte": "Svelte",
|
|
454
|
-
"vite": "Vite",
|
|
455
|
-
"express": "Express",
|
|
456
|
-
"nestjs": "NestJS",
|
|
457
|
-
}
|
|
458
|
-
for key, label in known.items():
|
|
459
|
-
if key in dep_names and label not in frameworks:
|
|
460
|
-
frameworks.append(label)
|
|
461
|
-
if (repo / "pyproject.toml").exists():
|
|
462
|
-
text = (repo / "pyproject.toml").read_text()
|
|
463
|
-
if "fastapi" in text.lower():
|
|
464
|
-
frameworks.append("FastAPI")
|
|
465
|
-
if "django" in text.lower():
|
|
466
|
-
frameworks.append("Django")
|
|
467
|
-
if "flask" in text.lower():
|
|
468
|
-
frameworks.append("Flask")
|
|
469
|
-
return frameworks
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
def detect_package_managers(repo):
|
|
473
|
-
package_managers = []
|
|
474
|
-
markers = {
|
|
475
|
-
"package-lock.json": "npm",
|
|
476
|
-
"pnpm-lock.yaml": "pnpm",
|
|
477
|
-
"yarn.lock": "yarn",
|
|
478
|
-
"bun.lockb": "bun",
|
|
479
|
-
"pyproject.toml": "uv/pip",
|
|
480
|
-
"requirements.txt": "pip",
|
|
481
|
-
"go.mod": "go",
|
|
482
|
-
"Cargo.toml": "cargo",
|
|
483
|
-
}
|
|
484
|
-
for marker, label in markers.items():
|
|
485
|
-
if (repo / marker).exists():
|
|
486
|
-
package_managers.append(label)
|
|
487
|
-
return package_managers
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
def list_repo_files(repo):
|
|
491
|
-
ignored = {".git", ".codex", "node_modules", ".next", "dist", "build", "__pycache__"}
|
|
492
|
-
results = []
|
|
493
|
-
for root, dirs, files in os.walk(repo):
|
|
494
|
-
dirs[:] = [d for d in dirs if d not in ignored]
|
|
495
|
-
for filename in files:
|
|
496
|
-
path = Path(root, filename)
|
|
497
|
-
results.append(str(path.relative_to(repo)))
|
|
498
|
-
return sorted(results)
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
def detect_existing_managed_files(repo):
|
|
502
|
-
managed = []
|
|
503
|
-
for relative_path in list(ROOT_FILES.keys()) + list(DOC_FILES.keys()):
|
|
504
|
-
path = repo / relative_path
|
|
505
|
-
if path.exists():
|
|
506
|
-
try:
|
|
507
|
-
if path.read_text().startswith(MANAGED_MARKER):
|
|
508
|
-
managed.append(relative_path)
|
|
509
|
-
except UnicodeDecodeError:
|
|
510
|
-
continue
|
|
511
|
-
return managed
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
def make_default_answers(analysis):
|
|
515
|
-
repo_name = analysis["project_name"]
|
|
516
|
-
frameworks = ", ".join(analysis["frameworks"]) or "Unknown"
|
|
517
|
-
has_frontend = analysis["has_frontend"]
|
|
518
|
-
frontend_scope = (
|
|
519
|
-
"User-facing or operator-facing frontend work is expected."
|
|
520
|
-
if has_frontend
|
|
521
|
-
else "No clear frontend surface was detected yet. Update this if a UI emerges."
|
|
522
|
-
)
|
|
523
|
-
frontend_validation_loop = (
|
|
524
|
-
"- Run local UI changes in a browser.\n"
|
|
525
|
-
"- Check desktop and mobile layouts when relevant.\n"
|
|
526
|
-
"- Verify key flows, empty states, and failure states.\n"
|
|
527
|
-
"- Record reusable UI findings in `docs/design-docs/`."
|
|
528
|
-
if has_frontend
|
|
529
|
-
else "- Validate interface changes in the relevant local runtime.\n"
|
|
530
|
-
"- Verify key flows, empty states, failure states, and cleanup behavior where applicable.\n"
|
|
531
|
-
"- Record reusable interface findings in `docs/design-docs/`."
|
|
532
|
-
)
|
|
533
|
-
defaults = {
|
|
534
|
-
"project_name": repo_name,
|
|
535
|
-
"project_summary": f"Summarize the main outcome that {repo_name} should deliver.",
|
|
536
|
-
"primary_users": "Describe the primary users, operators, or internal teams.",
|
|
537
|
-
"deployment_targets": "Describe the main runtime or deployment targets.",
|
|
538
|
-
"product_domain": "Describe the product domain in one line.",
|
|
539
|
-
"reliability_targets": "Describe uptime, failure tolerance, recovery expectations, and required validation loops.",
|
|
540
|
-
"security_constraints": "Describe auth, secrets, compliance, sensitive data, and review constraints.",
|
|
541
|
-
"frontend_stack_notes": (
|
|
542
|
-
f"Detected frameworks: {frameworks}. Describe UX expectations, supported environments, and review rules."
|
|
543
|
-
if has_frontend
|
|
544
|
-
else "No frontend detected. Replace this if the repo includes UI work."
|
|
545
|
-
),
|
|
546
|
-
"quality_focus": "List the product areas and architectural layers that deserve the strictest quality bar.",
|
|
547
|
-
"frontend_scope": frontend_scope,
|
|
548
|
-
"frontend_validation_loop": frontend_validation_loop,
|
|
549
|
-
}
|
|
550
|
-
return defaults
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
def fill_template(template, answers, analysis):
|
|
554
|
-
merged = {}
|
|
555
|
-
merged.update(make_default_answers(analysis))
|
|
556
|
-
merged.update(answers)
|
|
557
|
-
merged.update(
|
|
558
|
-
{
|
|
559
|
-
"marker": MANAGED_MARKER,
|
|
560
|
-
"languages": ", ".join(analysis["languages"]) or "Unknown",
|
|
561
|
-
"package_managers": ", ".join(analysis["package_managers"]) or "Unknown",
|
|
562
|
-
"frameworks": ", ".join(analysis["frameworks"]) or "Unknown",
|
|
563
|
-
}
|
|
564
|
-
)
|
|
565
|
-
return template.format(**merged)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
def ensure_parent(path):
|
|
569
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
def slugify(value):
|
|
573
|
-
normalized = re.sub(r"[^a-z0-9]+", "-", value.strip().lower()).strip("-")
|
|
574
|
-
return normalized or "task"
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
def find_section(lines, heading):
|
|
578
|
-
target = heading.strip().lower()
|
|
579
|
-
for index, line in enumerate(lines):
|
|
580
|
-
if line.strip().lower() == target:
|
|
581
|
-
return index
|
|
582
|
-
return None
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def extract_knowledge_items(text):
|
|
586
|
-
lines = text.splitlines()
|
|
587
|
-
section_index = find_section(lines, "## Durable Knowledge To Capture")
|
|
588
|
-
if section_index is None:
|
|
589
|
-
return []
|
|
590
|
-
items = []
|
|
591
|
-
for line in lines[section_index + 1 :]:
|
|
592
|
-
if line.startswith("## "):
|
|
593
|
-
break
|
|
594
|
-
stripped = line.strip()
|
|
595
|
-
if stripped.startswith("- ["):
|
|
596
|
-
items.append(stripped)
|
|
597
|
-
return items
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
def knowledge_id_for(fact, destination):
|
|
601
|
-
digest = hashlib.sha1(f"{clean_destination_text(destination)}\0{clean_fact_text(fact)}".encode()).hexdigest()
|
|
602
|
-
return f"hk-{digest[:10]}"
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
def parse_knowledge_item(item):
|
|
606
|
-
match = re.match(
|
|
607
|
-
r"- \[(?P<status>[ xX])\]\s+"
|
|
608
|
-
r"(?:\[(?:id|kid):(?P<id>[A-Za-z0-9_.:-]+)\]\s+)?"
|
|
609
|
-
r"(?P<fact>.*?)\s+->\s+"
|
|
610
|
-
r"(?P<destination>[^|]+?)"
|
|
611
|
-
r"(?:\s+\|\s+evidence:\s+(?P<evidence>.+))?$",
|
|
612
|
-
item.strip(),
|
|
613
|
-
)
|
|
614
|
-
if not match:
|
|
615
|
-
return None
|
|
616
|
-
return {
|
|
617
|
-
"status": "closed" if match.group("status").lower() == "x" else "open",
|
|
618
|
-
"id": match.group("id"),
|
|
619
|
-
"fact": clean_fact_text(match.group("fact")),
|
|
620
|
-
"destination": clean_destination_text(match.group("destination")),
|
|
621
|
-
"evidence": clean_fact_text(match.group("evidence")) if match.group("evidence") else None,
|
|
622
|
-
"raw": item,
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
def clean_fact_text(value):
|
|
627
|
-
cleaned = value.strip()
|
|
628
|
-
cleaned = cleaned.replace("`", "")
|
|
629
|
-
cleaned = re.sub(r"\s+", " ", cleaned)
|
|
630
|
-
return cleaned.strip()
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
def clean_destination_text(value):
|
|
634
|
-
return value.strip().strip("`")
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
def replace_completion_notes(text, summary):
|
|
638
|
-
lines = text.splitlines()
|
|
639
|
-
section_index = find_section(lines, "## Completion Notes")
|
|
640
|
-
if section_index is None:
|
|
641
|
-
return text.rstrip() + "\n\n## Completion Notes\n\n" + summary + "\n"
|
|
642
|
-
end_index = len(lines)
|
|
643
|
-
for index in range(section_index + 1, len(lines)):
|
|
644
|
-
if lines[index].startswith("## "):
|
|
645
|
-
end_index = index
|
|
646
|
-
break
|
|
647
|
-
new_lines = lines[: section_index + 1] + ["", summary] + lines[end_index:]
|
|
648
|
-
return "\n".join(new_lines).rstrip() + "\n"
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
def append_knowledge_item(plan_path, fact, destination):
|
|
652
|
-
text = plan_path.read_text()
|
|
653
|
-
lines = text.splitlines()
|
|
654
|
-
section_index = find_section(lines, "## Durable Knowledge To Capture")
|
|
655
|
-
if section_index is None:
|
|
656
|
-
raise ValueError("Plan is missing '## Durable Knowledge To Capture'")
|
|
657
|
-
placeholder = "- [ ] Add durable facts here as they emerge -> <destination-doc>"
|
|
658
|
-
filtered_lines = [line for line in lines if line.strip() != placeholder]
|
|
659
|
-
insert_index = section_index + 1
|
|
660
|
-
while insert_index < len(filtered_lines) and not filtered_lines[insert_index].startswith("## "):
|
|
661
|
-
insert_index += 1
|
|
662
|
-
item_id = knowledge_id_for(fact, destination)
|
|
663
|
-
item = f"- [ ] [id:{item_id}] {fact} -> {destination}"
|
|
664
|
-
updated_lines = filtered_lines[:insert_index] + [item] + filtered_lines[insert_index:]
|
|
665
|
-
plan_path.write_text("\n".join(updated_lines).rstrip() + "\n")
|
|
666
|
-
return item, item_id
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def mark_knowledge_items_closed(text):
|
|
670
|
-
lines = text.splitlines()
|
|
671
|
-
updated = []
|
|
672
|
-
for line in lines:
|
|
673
|
-
if line.strip().startswith("- [ ]"):
|
|
674
|
-
updated.append(line.replace("- [ ]", "- [x]", 1))
|
|
675
|
-
else:
|
|
676
|
-
updated.append(line)
|
|
677
|
-
return "\n".join(updated).rstrip() + "\n"
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
def destination_contains_fact(repo, destination, fact):
|
|
681
|
-
target = repo / destination
|
|
682
|
-
if not target.exists() or not target.is_file():
|
|
683
|
-
return False
|
|
684
|
-
try:
|
|
685
|
-
return normalize_fact_for_match(fact) in normalize_fact_for_match(target.read_text())
|
|
686
|
-
except UnicodeDecodeError:
|
|
687
|
-
return False
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
def normalize_fact_for_match(value):
|
|
691
|
-
normalized = value.replace("`", "")
|
|
692
|
-
normalized = re.sub(r"\s+", " ", normalized)
|
|
693
|
-
normalized = normalized.strip()
|
|
694
|
-
normalized = re.sub(r"[.。]+$", "", normalized)
|
|
695
|
-
return normalized
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
def append_fact_to_destination(repo, destination, fact):
|
|
699
|
-
target = repo / destination
|
|
700
|
-
ensure_parent(target)
|
|
701
|
-
existing = ""
|
|
702
|
-
if target.exists():
|
|
703
|
-
existing = target.read_text()
|
|
704
|
-
separator = "\n" if existing.endswith("\n") or not existing else "\n\n"
|
|
705
|
-
target.write_text(existing + separator + fact + "\n")
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
def close_knowledge_line(line, evidence=None):
|
|
709
|
-
updated = line.replace("- [ ]", "- [x]", 1)
|
|
710
|
-
if evidence and "| evidence:" not in updated:
|
|
711
|
-
updated = f"{updated} | evidence: {evidence}"
|
|
712
|
-
return updated
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
def mark_single_knowledge_item_written(
|
|
716
|
-
repo,
|
|
717
|
-
plan_path,
|
|
718
|
-
fact_text=None,
|
|
719
|
-
destination=None,
|
|
720
|
-
append=False,
|
|
721
|
-
knowledge_id=None,
|
|
722
|
-
evidence=None,
|
|
723
|
-
):
|
|
724
|
-
if not fact_text and not knowledge_id:
|
|
725
|
-
raise ValueError("Provide either --id or --fact to mark knowledge as written")
|
|
726
|
-
lines = plan_path.read_text().splitlines()
|
|
727
|
-
target = clean_fact_text(fact_text) if fact_text else None
|
|
728
|
-
target_destination = clean_destination_text(destination) if destination else None
|
|
729
|
-
target_evidence = clean_fact_text(evidence) if evidence else None
|
|
730
|
-
replaced = False
|
|
731
|
-
updated = []
|
|
732
|
-
for line in lines:
|
|
733
|
-
stripped = line.strip()
|
|
734
|
-
parsed = parse_knowledge_item(stripped)
|
|
735
|
-
if not parsed:
|
|
736
|
-
updated.append(line)
|
|
737
|
-
continue
|
|
738
|
-
destination_matches = target_destination is None or parsed["destination"] == target_destination
|
|
739
|
-
fact_matches = target is not None and normalize_fact_for_match(target) == normalize_fact_for_match(parsed["fact"])
|
|
740
|
-
id_matches = knowledge_id is not None and parsed["id"] == knowledge_id
|
|
741
|
-
if stripped.startswith("- [ ]") and (id_matches or fact_matches) and destination_matches and not replaced:
|
|
742
|
-
parsed_destination = parsed["destination"]
|
|
743
|
-
if not parsed_destination:
|
|
744
|
-
raise ValueError("Destination is required to verify durable knowledge")
|
|
745
|
-
verification_text = target_evidence or target or parsed["fact"]
|
|
746
|
-
if not destination_contains_fact(repo, parsed_destination, verification_text):
|
|
747
|
-
if append:
|
|
748
|
-
append_fact_to_destination(repo, parsed_destination, verification_text)
|
|
749
|
-
else:
|
|
750
|
-
raise ValueError(
|
|
751
|
-
f"Destination {parsed_destination} does not contain verification text: {verification_text}. "
|
|
752
|
-
"Write it there first, pass --evidence with text present in the doc, or re-run with --append."
|
|
753
|
-
)
|
|
754
|
-
updated.append(close_knowledge_line(line, evidence=target_evidence))
|
|
755
|
-
replaced = True
|
|
756
|
-
else:
|
|
757
|
-
updated.append(line)
|
|
758
|
-
if not replaced:
|
|
759
|
-
target_description = f"id: {knowledge_id}" if knowledge_id else f"fact: {fact_text}"
|
|
760
|
-
raise ValueError(f"Open knowledge item not found for {target_description}")
|
|
761
|
-
plan_path.write_text("\n".join(updated).rstrip() + "\n")
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
def should_write(path, refresh_managed, force):
|
|
765
|
-
if not path.exists():
|
|
766
|
-
return True
|
|
767
|
-
if force:
|
|
768
|
-
return True
|
|
769
|
-
try:
|
|
770
|
-
is_managed = path.read_text().startswith(MANAGED_MARKER)
|
|
771
|
-
except UnicodeDecodeError:
|
|
772
|
-
return False
|
|
773
|
-
if refresh_managed and is_managed:
|
|
774
|
-
return True
|
|
775
|
-
return False
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
def write_scaffold(repo, analysis, answers, refresh_managed=False, force=False):
|
|
779
|
-
written = []
|
|
780
|
-
skipped = []
|
|
781
|
-
all_templates = {}
|
|
782
|
-
all_templates.update(ROOT_FILES)
|
|
783
|
-
all_templates.update(DOC_FILES)
|
|
784
|
-
|
|
785
|
-
for relative_path, template in all_templates.items():
|
|
786
|
-
target = repo / relative_path
|
|
787
|
-
if should_write(target, refresh_managed, force):
|
|
788
|
-
ensure_parent(target)
|
|
789
|
-
content = fill_template(template, answers, analysis)
|
|
790
|
-
target.write_text(content)
|
|
791
|
-
written.append(relative_path)
|
|
792
|
-
else:
|
|
793
|
-
skipped.append(relative_path)
|
|
794
|
-
return written, skipped
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
def active_plan_dir(repo):
|
|
798
|
-
return repo / "docs" / "exec-plans" / "active"
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
def completed_plan_dir(repo):
|
|
802
|
-
return repo / "docs" / "exec-plans" / "completed"
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
def create_plan(repo, slug, goal):
|
|
806
|
-
plan_dir = active_plan_dir(repo)
|
|
807
|
-
plan_dir.mkdir(parents=True, exist_ok=True)
|
|
808
|
-
filename = f"{datetime.now(UTC).strftime('%Y-%m-%d')}-{slugify(slug)}.md"
|
|
809
|
-
plan_path = plan_dir / filename
|
|
810
|
-
if plan_path.exists():
|
|
811
|
-
raise FileExistsError(f"Plan already exists: {plan_path}")
|
|
812
|
-
title = slug.replace("-", " ").strip() or "task"
|
|
813
|
-
content = PLAN_TEMPLATE.format(
|
|
814
|
-
title=title.title(),
|
|
815
|
-
goal=goal,
|
|
816
|
-
knowledge_section="- [ ] Add durable facts here as they emerge -> <destination-doc>",
|
|
817
|
-
)
|
|
818
|
-
plan_path.write_text(content)
|
|
819
|
-
return plan_path
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
def close_plan(repo, plan_relative_path, summary, force):
|
|
823
|
-
plan_path = repo / plan_relative_path
|
|
824
|
-
if not plan_path.exists():
|
|
825
|
-
raise FileNotFoundError(f"Plan not found: {plan_path}")
|
|
826
|
-
text = plan_path.read_text()
|
|
827
|
-
open_items = [item for item in extract_knowledge_items(text) if item.startswith("- [ ]")]
|
|
828
|
-
if open_items and not force:
|
|
829
|
-
raise RuntimeError(
|
|
830
|
-
"Cannot close plan with unresolved durable knowledge items:\n" + "\n".join(open_items)
|
|
831
|
-
)
|
|
832
|
-
updated_text = replace_completion_notes(mark_knowledge_items_closed(text), summary)
|
|
833
|
-
completed_dir = completed_plan_dir(repo)
|
|
834
|
-
completed_dir.mkdir(parents=True, exist_ok=True)
|
|
835
|
-
destination = completed_dir / plan_path.name
|
|
836
|
-
destination.write_text(updated_text)
|
|
837
|
-
plan_path.unlink()
|
|
838
|
-
return destination, open_items
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
def check_harness(repo):
|
|
842
|
-
required_files = [
|
|
843
|
-
"AGENTS.md",
|
|
844
|
-
"ARCHITECTURE.md",
|
|
845
|
-
"docs/PLANS.md",
|
|
846
|
-
"docs/QUALITY_SCORE.md",
|
|
847
|
-
"docs/RELIABILITY.md",
|
|
848
|
-
"docs/SECURITY.md",
|
|
849
|
-
"docs/exec-plans/active/README.md",
|
|
850
|
-
"docs/exec-plans/active/_template.md",
|
|
851
|
-
"docs/exec-plans/completed/README.md",
|
|
852
|
-
"docs/sops/encode-unseen-knowledge.md",
|
|
853
|
-
]
|
|
854
|
-
issues = []
|
|
855
|
-
for relative_path in required_files:
|
|
856
|
-
if not (repo / relative_path).exists():
|
|
857
|
-
issues.append(
|
|
858
|
-
{
|
|
859
|
-
"severity": "error",
|
|
860
|
-
"code": "missing-required-file",
|
|
861
|
-
"path": relative_path,
|
|
862
|
-
"message": f"Required harness file is missing: {relative_path}",
|
|
863
|
-
}
|
|
864
|
-
)
|
|
865
|
-
|
|
866
|
-
active_dir = active_plan_dir(repo)
|
|
867
|
-
if active_dir.exists():
|
|
868
|
-
for plan_path in sorted(active_dir.glob("*.md")):
|
|
869
|
-
if plan_path.name in {"README.md", "_template.md"}:
|
|
870
|
-
continue
|
|
871
|
-
relative_plan = str(plan_path.relative_to(repo))
|
|
872
|
-
for item in extract_knowledge_items(plan_path.read_text()):
|
|
873
|
-
parsed = parse_knowledge_item(item)
|
|
874
|
-
if not parsed:
|
|
875
|
-
issues.append(
|
|
876
|
-
{
|
|
877
|
-
"severity": "error",
|
|
878
|
-
"code": "unparseable-knowledge-item",
|
|
879
|
-
"path": relative_plan,
|
|
880
|
-
"message": f"Knowledge item is not parseable: {item}",
|
|
881
|
-
}
|
|
882
|
-
)
|
|
883
|
-
continue
|
|
884
|
-
if parsed["status"] == "open":
|
|
885
|
-
issues.append(
|
|
886
|
-
{
|
|
887
|
-
"severity": "error",
|
|
888
|
-
"code": "open-durable-knowledge",
|
|
889
|
-
"path": relative_plan,
|
|
890
|
-
"destination": parsed["destination"],
|
|
891
|
-
"message": f"Durable knowledge is still open: {parsed['fact']}",
|
|
892
|
-
}
|
|
893
|
-
)
|
|
894
|
-
else:
|
|
895
|
-
verification_text = parsed["evidence"] or parsed["fact"]
|
|
896
|
-
if destination_contains_fact(repo, parsed["destination"], verification_text):
|
|
897
|
-
continue
|
|
898
|
-
issues.append(
|
|
899
|
-
{
|
|
900
|
-
"severity": "error",
|
|
901
|
-
"code": "missing-written-knowledge",
|
|
902
|
-
"path": relative_plan,
|
|
903
|
-
"destination": parsed["destination"],
|
|
904
|
-
"message": f"Marked knowledge evidence is missing from destination: {verification_text}",
|
|
905
|
-
}
|
|
906
|
-
)
|
|
907
|
-
|
|
908
|
-
return {
|
|
909
|
-
"repo": str(repo),
|
|
910
|
-
"status": "pass" if not issues else "fail",
|
|
911
|
-
"issue_count": len(issues),
|
|
912
|
-
"issues": issues,
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
def analyze_repo(repo):
|
|
917
|
-
files = list_repo_files(repo)
|
|
918
|
-
languages = detect_languages(files)
|
|
919
|
-
frameworks = detect_frameworks(repo)
|
|
920
|
-
package_managers = detect_package_managers(repo)
|
|
921
|
-
has_frontend = any(name in frameworks for name in ["Next.js", "React", "Vue", "Svelte", "Vite"]) or any(
|
|
922
|
-
file.endswith((".tsx", ".jsx", ".css", ".scss")) for file in files
|
|
923
|
-
)
|
|
924
|
-
existing_managed = detect_existing_managed_files(repo)
|
|
925
|
-
existing_harness = [
|
|
926
|
-
file for file in ["AGENTS.md", "ARCHITECTURE.md", "docs/PLANS.md", "docs/SECURITY.md"] if (repo / file).exists()
|
|
927
|
-
]
|
|
928
|
-
missing_exec_plan_state = [
|
|
929
|
-
path
|
|
930
|
-
for path in [
|
|
931
|
-
"docs/exec-plans/active/README.md",
|
|
932
|
-
"docs/exec-plans/active/_template.md",
|
|
933
|
-
"docs/exec-plans/completed/README.md",
|
|
934
|
-
]
|
|
935
|
-
if not (repo / path).exists()
|
|
936
|
-
]
|
|
937
|
-
missing_sops = [
|
|
938
|
-
path
|
|
939
|
-
for path in [
|
|
940
|
-
"docs/sops/layered-domain-architecture-setup.md",
|
|
941
|
-
"docs/sops/encode-unseen-knowledge.md",
|
|
942
|
-
"docs/sops/local-observability-feedback-loop.md",
|
|
943
|
-
"docs/sops/chrome-devtools-ui-validation-loop.md",
|
|
944
|
-
]
|
|
945
|
-
if not (repo / path).exists()
|
|
946
|
-
]
|
|
947
|
-
durable_knowledge_targets = [
|
|
948
|
-
"ARCHITECTURE.md",
|
|
949
|
-
"docs/product-specs/",
|
|
950
|
-
"docs/design-docs/",
|
|
951
|
-
"docs/RELIABILITY.md",
|
|
952
|
-
"docs/SECURITY.md",
|
|
953
|
-
"docs/references/",
|
|
954
|
-
]
|
|
955
|
-
|
|
956
|
-
inferred_answers = {
|
|
957
|
-
"project_name": repo.name,
|
|
958
|
-
"languages": languages,
|
|
959
|
-
"frameworks": frameworks,
|
|
960
|
-
"package_managers": package_managers,
|
|
961
|
-
"frontend_scope": (
|
|
962
|
-
"A frontend surface likely exists."
|
|
963
|
-
if has_frontend
|
|
964
|
-
else "No obvious frontend surface detected from the repository."
|
|
965
|
-
),
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
human_confirmations = []
|
|
969
|
-
for question in QUESTION_CATALOG:
|
|
970
|
-
if question["id"] == "frontend_stack_notes" and not has_frontend:
|
|
971
|
-
continue
|
|
972
|
-
human_confirmations.append(question)
|
|
973
|
-
|
|
974
|
-
analysis = {
|
|
975
|
-
"project_name": repo.name,
|
|
976
|
-
"repo_path": str(repo.resolve()),
|
|
977
|
-
"languages": languages,
|
|
978
|
-
"frameworks": frameworks,
|
|
979
|
-
"package_managers": package_managers,
|
|
980
|
-
"has_frontend": has_frontend,
|
|
981
|
-
"inferred_answers": inferred_answers,
|
|
982
|
-
"existing_harness_files": existing_harness,
|
|
983
|
-
"existing_managed_files": existing_managed,
|
|
984
|
-
"missing_exec_plan_state": missing_exec_plan_state,
|
|
985
|
-
"missing_sops": missing_sops,
|
|
986
|
-
"durable_knowledge_targets": durable_knowledge_targets,
|
|
987
|
-
"human_confirmations": human_confirmations,
|
|
988
|
-
"recommended_action": "update" if existing_harness or existing_managed else "init",
|
|
989
|
-
"notes": [
|
|
990
|
-
"Ask the human only the confirmations that the repository cannot answer safely.",
|
|
991
|
-
"If unmanaged harness files already exist, preserve them unless the human explicitly requests replacement.",
|
|
992
|
-
"Create execution-plan state before expecting agents to keep multi-step work synchronized.",
|
|
993
|
-
"Use SOPs to turn recurring architecture, UI, observability, and knowledge-capture work into mechanical loops.",
|
|
994
|
-
"Write durable facts into permanent docs instead of leaving them trapped inside plans or chat history.",
|
|
995
|
-
],
|
|
996
|
-
}
|
|
997
|
-
return analysis
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
def load_json(path):
|
|
1001
|
-
return json.loads(Path(path).read_text())
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
def write_json(path, payload):
|
|
1005
|
-
output = json.dumps(payload, indent=2, ensure_ascii=False) + "\n"
|
|
1006
|
-
if path:
|
|
1007
|
-
Path(path).write_text(output)
|
|
1008
|
-
else:
|
|
1009
|
-
print(output, end="")
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
def command_analyze(args):
|
|
1013
|
-
repo = Path(args.repo).resolve()
|
|
1014
|
-
analysis = analyze_repo(repo)
|
|
1015
|
-
write_json(args.output, analysis)
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
def command_sample_answers(args):
|
|
1019
|
-
analysis = load_json(args.analysis)
|
|
1020
|
-
payload = make_default_answers(analysis)
|
|
1021
|
-
write_json(args.output, payload)
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
def command_init_or_update(args, refresh_managed):
|
|
1025
|
-
repo = Path(args.repo).resolve()
|
|
1026
|
-
analysis = analyze_repo(repo)
|
|
1027
|
-
answers = load_json(args.answers)
|
|
1028
|
-
written, skipped = write_scaffold(repo, analysis, answers, refresh_managed=refresh_managed, force=args.force)
|
|
1029
|
-
result = {
|
|
1030
|
-
"repo": str(repo),
|
|
1031
|
-
"written": written,
|
|
1032
|
-
"skipped": skipped,
|
|
1033
|
-
"mode": "update" if refresh_managed else "init",
|
|
1034
|
-
}
|
|
1035
|
-
write_json(args.output, result)
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
def command_plan_start(args):
|
|
1039
|
-
repo = Path(args.repo).resolve()
|
|
1040
|
-
plan_path = create_plan(repo, args.slug, args.goal)
|
|
1041
|
-
result = {"repo": str(repo), "plan": str(plan_path), "status": "created"}
|
|
1042
|
-
write_json(args.output, result)
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
def command_knowledge_log(args):
|
|
1046
|
-
repo = Path(args.repo).resolve()
|
|
1047
|
-
plan_path = repo / args.plan
|
|
1048
|
-
if not plan_path.exists():
|
|
1049
|
-
raise FileNotFoundError(f"Plan not found: {plan_path}")
|
|
1050
|
-
item, item_id = append_knowledge_item(plan_path, args.fact, args.destination)
|
|
1051
|
-
result = {"repo": str(repo), "plan": str(plan_path), "id": item_id, "logged": item}
|
|
1052
|
-
write_json(args.output, result)
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
def command_plan_close(args):
|
|
1056
|
-
repo = Path(args.repo).resolve()
|
|
1057
|
-
destination, unresolved = close_plan(repo, args.plan, args.summary, args.force)
|
|
1058
|
-
result = {
|
|
1059
|
-
"repo": str(repo),
|
|
1060
|
-
"closed_plan": str(destination),
|
|
1061
|
-
"unresolved_items_forced": unresolved,
|
|
1062
|
-
"status": "closed",
|
|
1063
|
-
}
|
|
1064
|
-
write_json(args.output, result)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
def command_knowledge_mark_written(args):
|
|
1068
|
-
repo = Path(args.repo).resolve()
|
|
1069
|
-
plan_path = repo / args.plan
|
|
1070
|
-
if not plan_path.exists():
|
|
1071
|
-
raise FileNotFoundError(f"Plan not found: {plan_path}")
|
|
1072
|
-
mark_single_knowledge_item_written(
|
|
1073
|
-
repo,
|
|
1074
|
-
plan_path,
|
|
1075
|
-
args.fact,
|
|
1076
|
-
args.destination,
|
|
1077
|
-
append=args.append,
|
|
1078
|
-
knowledge_id=args.id,
|
|
1079
|
-
evidence=args.evidence,
|
|
1080
|
-
)
|
|
1081
|
-
result = {
|
|
1082
|
-
"repo": str(repo),
|
|
1083
|
-
"plan": str(plan_path),
|
|
1084
|
-
"marked_written": args.id or args.fact,
|
|
1085
|
-
"destination": args.destination,
|
|
1086
|
-
"evidence": args.evidence,
|
|
1087
|
-
}
|
|
1088
|
-
write_json(args.output, result)
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
def command_check(args):
|
|
1092
|
-
repo = Path(args.repo).resolve()
|
|
1093
|
-
result = check_harness(repo)
|
|
1094
|
-
write_json(args.output, result)
|
|
1095
|
-
if result["status"] != "pass":
|
|
1096
|
-
raise SystemExit(1)
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
def build_parser():
|
|
1100
|
-
parser = argparse.ArgumentParser(description="Manage the harness repo scaffold.")
|
|
1101
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1102
|
-
|
|
1103
|
-
analyze = subparsers.add_parser("analyze")
|
|
1104
|
-
analyze.add_argument("--repo", required=True)
|
|
1105
|
-
analyze.add_argument("--output")
|
|
1106
|
-
analyze.set_defaults(func=command_analyze)
|
|
1107
|
-
|
|
1108
|
-
sample_answers = subparsers.add_parser("sample-answers")
|
|
1109
|
-
sample_answers.add_argument("--analysis", required=True)
|
|
1110
|
-
sample_answers.add_argument("--output")
|
|
1111
|
-
sample_answers.set_defaults(func=command_sample_answers)
|
|
1112
|
-
|
|
1113
|
-
init = subparsers.add_parser("init")
|
|
1114
|
-
init.add_argument("--repo", required=True)
|
|
1115
|
-
init.add_argument("--answers", required=True)
|
|
1116
|
-
init.add_argument("--output")
|
|
1117
|
-
init.add_argument("--force", action="store_true")
|
|
1118
|
-
init.set_defaults(func=lambda args: command_init_or_update(args, refresh_managed=False))
|
|
1119
|
-
|
|
1120
|
-
update = subparsers.add_parser("update")
|
|
1121
|
-
update.add_argument("--repo", required=True)
|
|
1122
|
-
update.add_argument("--answers", required=True)
|
|
1123
|
-
update.add_argument("--output")
|
|
1124
|
-
update.add_argument("--refresh-managed", action="store_true")
|
|
1125
|
-
update.add_argument("--force", action="store_true")
|
|
1126
|
-
update.set_defaults(
|
|
1127
|
-
func=lambda args: command_init_or_update(
|
|
1128
|
-
args, refresh_managed=args.refresh_managed or args.force
|
|
1129
|
-
)
|
|
1130
|
-
)
|
|
1131
|
-
|
|
1132
|
-
plan_start = subparsers.add_parser("plan-start")
|
|
1133
|
-
plan_start.add_argument("--repo", required=True)
|
|
1134
|
-
plan_start.add_argument("--slug", required=True)
|
|
1135
|
-
plan_start.add_argument("--goal", required=True)
|
|
1136
|
-
plan_start.add_argument("--output")
|
|
1137
|
-
plan_start.set_defaults(func=command_plan_start)
|
|
1138
|
-
|
|
1139
|
-
knowledge_log = subparsers.add_parser("knowledge-log")
|
|
1140
|
-
knowledge_log.add_argument("--repo", required=True)
|
|
1141
|
-
knowledge_log.add_argument("--plan", required=True)
|
|
1142
|
-
knowledge_log.add_argument("--fact", required=True)
|
|
1143
|
-
knowledge_log.add_argument("--destination", required=True)
|
|
1144
|
-
knowledge_log.add_argument("--output")
|
|
1145
|
-
knowledge_log.set_defaults(func=command_knowledge_log)
|
|
1146
|
-
|
|
1147
|
-
knowledge_mark_written = subparsers.add_parser("knowledge-mark-written")
|
|
1148
|
-
knowledge_mark_written.add_argument("--repo", required=True)
|
|
1149
|
-
knowledge_mark_written.add_argument("--plan", required=True)
|
|
1150
|
-
knowledge_mark_written.add_argument("--id")
|
|
1151
|
-
knowledge_mark_written.add_argument("--fact")
|
|
1152
|
-
knowledge_mark_written.add_argument("--destination")
|
|
1153
|
-
knowledge_mark_written.add_argument("--evidence")
|
|
1154
|
-
knowledge_mark_written.add_argument("--append", action="store_true")
|
|
1155
|
-
knowledge_mark_written.add_argument("--output")
|
|
1156
|
-
knowledge_mark_written.set_defaults(func=command_knowledge_mark_written)
|
|
1157
|
-
|
|
1158
|
-
plan_close = subparsers.add_parser("plan-close")
|
|
1159
|
-
plan_close.add_argument("--repo", required=True)
|
|
1160
|
-
plan_close.add_argument("--plan", required=True)
|
|
1161
|
-
plan_close.add_argument("--summary", required=True)
|
|
1162
|
-
plan_close.add_argument("--force", action="store_true")
|
|
1163
|
-
plan_close.add_argument("--output")
|
|
1164
|
-
plan_close.set_defaults(func=command_plan_close)
|
|
1165
|
-
|
|
1166
|
-
check = subparsers.add_parser("check")
|
|
1167
|
-
check.add_argument("--repo", required=True)
|
|
1168
|
-
check.add_argument("--output")
|
|
1169
|
-
check.set_defaults(func=command_check)
|
|
1170
|
-
|
|
1171
|
-
return parser
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
def main():
|
|
1175
|
-
parser = build_parser()
|
|
1176
|
-
args = parser.parse_args()
|
|
1177
|
-
args.func(args)
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if __name__ == "__main__":
|
|
1181
|
-
main()
|