@hallucination-studio/harness-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1181 @@
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()