@surlymochan/evalid 0.0.0-stage → 0.3.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/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/RELEASE_NOTES.md +102 -0
- package/atoms/registry.json +758 -0
- package/bin/xflow.js +7208 -0
- package/docs/adoption/README.md +128 -0
- package/docs/adoption/openflow-release-hardening.md +61 -0
- package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
- package/docs/compatibility.md +52 -0
- package/docs/competitive-benchmark.md +108 -0
- package/docs/corps-operator-guide.md +75 -0
- package/docs/demo-proof.md +126 -0
- package/docs/examples-gallery.md +115 -0
- package/docs/fixtures/tracker-item.json +7 -0
- package/docs/goal-vs-codex.md +129 -0
- package/docs/gstack-comparison.md +88 -0
- package/docs/install-upgrade.md +133 -0
- package/docs/integrations.md +49 -0
- package/docs/launch-demo.md +123 -0
- package/docs/launch-dossier.md +180 -0
- package/docs/methodology.md +149 -0
- package/docs/npm-publish-handoff.md +83 -0
- package/docs/openspec-migration.md +106 -0
- package/docs/public-benchmark.md +68 -0
- package/docs/public-release.md +145 -0
- package/docs/quality-assessment.md +139 -0
- package/docs/quickstart.md +156 -0
- package/docs/reviewer-guide.md +43 -0
- package/docs/spec-kit-benchmark.md +73 -0
- package/docs/super-assistant-comparison.md +106 -0
- package/docs/superpowers-comparison.md +82 -0
- package/docs/team-adoption.md +123 -0
- package/docs/tooling-matrix.md +91 -0
- package/docs/walkthrough.md +76 -0
- package/docs/workflow-manual.md +1125 -0
- package/package.json +83 -4
- package/prompts/pencil-draft.md +46 -0
- package/prompts/pencil-refine.md +46 -0
- package/prompts/phase-artifact.md +24 -0
- package/prompts/phase-execution.md +32 -0
- package/schemas/competitor-reconstruction-contract.schema.json +91 -0
- package/schemas/llm-design-review-aggregate.schema.json +83 -0
- package/schemas/visual-benchmark.schema.json +978 -0
- package/schemas/workflow.schema.json +100 -0
- package/scripts/run-tests.mjs +23 -0
- package/scripts/takein_digest.py +139 -0
- package/specs/README.md +118 -0
- package/specs/architecture.md +36 -0
- package/specs/product.md +33 -0
- package/specs/workflow.md +349 -0
- package/src/agent_team/atoms/codex-app-bridge.js +8 -0
- package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
- package/src/agent_team/atoms/design/contract-freeze.js +8 -0
- package/src/agent_team/atoms/design/generation-contract.js +442 -0
- package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
- package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
- package/src/agent_team/atoms/patch-challenge.js +19 -0
- package/src/agent_team/atoms/pencil/accept.js +9 -0
- package/src/agent_team/atoms/pencil/draft.js +365 -0
- package/src/agent_team/atoms/pencil/refine.js +229 -0
- package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
- package/src/agent_team/atoms/team-run.js +380 -0
- package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
- package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
- package/src/agent_team/atoms/visual/benchmark.js +1652 -0
- package/src/agent_team/atoms/visual/design-selection.js +23 -0
- package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
- package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
- package/src/agent_team/atoms/visual/image-diff.js +186 -0
- package/src/agent_team/atoms/visual/layout-competition.js +8 -0
- package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
- package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
- package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
- package/src/agent_team/atoms/visual/review.js +77 -0
- package/src/agent_team/atoms/visual/visual-report.js +197 -0
- package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
- package/src/agent_team/common.js +311 -0
- package/src/core/atoms/agent-invoke.js +684 -0
- package/src/core/atoms/event/ingest.js +30 -0
- package/src/core/atoms/experience/digest.js +55 -0
- package/src/core/atoms/gate/artifact-verify.js +83 -0
- package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
- package/src/core/atoms/gate/evidence-collect.js +64 -0
- package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
- package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
- package/src/core/atoms/gate/workbench.js +79 -0
- package/src/core/atoms/intake/classify.js +15 -0
- package/src/core/atoms/intake/design-skill-select.js +16 -0
- package/src/core/atoms/intake/preview-or-refine.js +19 -0
- package/src/core/atoms/intake/recommend.js +20 -0
- package/src/core/atoms/intake/research-same-category.js +19 -0
- package/src/core/atoms/intervention/create.js +30 -0
- package/src/core/atoms/memory/manage.js +27 -0
- package/src/core/atoms/memory/mount.js +13 -0
- package/src/core/atoms/mission/create.js +19 -0
- package/src/core/atoms/mission/list.js +14 -0
- package/src/core/atoms/mission/show.js +26 -0
- package/src/core/atoms/notify/checkpoint.js +46 -0
- package/src/core/atoms/phase-run/complete.js +42 -0
- package/src/core/atoms/phase-run/fail.js +40 -0
- package/src/core/atoms/phase-run/persist.js +39 -0
- package/src/core/atoms/phase-run/start.js +30 -0
- package/src/core/atoms/plugin/run.js +76 -0
- package/src/core/atoms/policy/gate.js +90 -0
- package/src/core/atoms/policy/resolve.js +11 -0
- package/src/core/atoms/storage/sync.js +57 -0
- package/src/core/atoms/workflow/advance.js +25 -0
- package/src/core/atoms/workflow/guard.js +127 -0
- package/src/core/atoms/workflow/state-load.js +37 -0
- package/src/core/bindings.js +37 -0
- package/src/core/change-artifacts.js +134 -0
- package/src/core/competitive-score.js +265 -0
- package/src/core/concurrency-policy.js +35 -0
- package/src/core/execution-log.js +108 -0
- package/src/core/intake-recommendation.js +134 -0
- package/src/core/memory-state.js +62 -0
- package/src/core/mission-state.js +780 -0
- package/src/core/pending-gates.js +66 -0
- package/src/core/phase-cost.js +116 -0
- package/src/core/phase-run-read.js +27 -0
- package/src/core/policy-overlay.js +180 -0
- package/src/core/state-store.js +201 -0
- package/src/core/workflow-executor.js +998 -0
- package/src/core/workflow-integrity.js +111 -0
- package/src/core/workflow-loader.js +163 -0
- package/src/core/workflow-rerun.js +84 -0
- package/src/core/workflow-state.js +119 -0
- package/src/server.js +1307 -0
- package/workflows/corps.yaml +479 -0
- package/workflows/yolo.yaml +233 -0
- package/xflow/README.md +76 -0
- package/xflow/aha/SKILL.md +82 -0
- package/xflow/aha/scripts/append_aha.py +50 -0
- package/xflow/archive/scripts/archive_change.py +338 -0
- package/xflow/archive/templates/merge-design.md +54 -0
- package/xflow/archive/templates/merge-workflow.md +26 -0
- package/xflow/atoms/__init__.py +6 -0
- package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
- package/xflow/atoms/a2_branch_create_and_link.py +42 -0
- package/xflow/atoms/a3_repo_doctor.py +11 -0
- package/xflow/atoms/a4_project_set_status.py +11 -0
- package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
- package/xflow/atoms/a6_pr_create.py +72 -0
- package/xflow/atoms/b1_change_scaffold.py +11 -0
- package/xflow/atoms/b2_status_read.py +16 -0
- package/xflow/atoms/b2b_status_write.py +20 -0
- package/xflow/atoms/b3_status_transition.py +27 -0
- package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
- package/xflow/atoms/b5_aha_append.py +11 -0
- package/xflow/atoms/b6_takein_digest.py +11 -0
- package/xflow/atoms/b6b_lookback_digest.py +11 -0
- package/xflow/atoms/b7_change_mission_bind.py +43 -0
- package/xflow/atoms/c1_mem.py +54 -0
- package/xflow/atoms/e6_gate_local_precheck.py +23 -0
- package/xflow/atoms/h1_design_lite_gate.py +74 -0
- package/xflow/atoms/i6a_tdd_run.py +104 -0
- package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
- package/xflow/atoms/i6c_tdd_quality_review.py +357 -0
- package/xflow/atoms/j1_tests_run.py +129 -0
- package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
- package/xflow/atoms/j4b_spec_delta_review.py +316 -0
- package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
- package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
- package/xflow/atoms/j6_cr_review.py +443 -0
- package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
- package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
- package/xflow/atoms/k3_handoff_refresh.py +14 -0
- package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
- package/xflow/atoms/k5_archive_publish.py +21 -0
- package/xflow/atoms/k6_aha_merge.py +42 -0
- package/xflow/atoms/tdd_lib.py +747 -0
- package/xflow/corps/SKILL.md +109 -0
- package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
- package/xflow/goal/SKILL.md +150 -0
- package/xflow/handoff/SKILL.md +95 -0
- package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
- package/xflow/lookback/SKILL.md +66 -0
- package/xflow/lookback/scripts/lookback_digest.py +46 -0
- package/xflow/openissue/scripts/open_issue_flow.py +438 -0
- package/xflow/plan/SKILL.md +38 -0
- package/xflow/ralph/SKILL.md +123 -0
- package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
- package/xflow/scripts/common.py +480 -0
- package/xflow/scripts/doctor.py +278 -0
- package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
- package/xflow/takein/SKILL.md +81 -0
- package/xflow/takein/scripts/takein_digest.py +88 -0
- package/xflow/yolo/SKILL.md +66 -0
- package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
- package/xflow/yolo/scripts/yolo_gate.py +312 -0
- package/xflow/yolo/templates/DESIGN.md +179 -0
package/xflow/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# xflow
|
|
2
|
+
|
|
3
|
+
Project-facing skill family for `evalid`, an executable delivery workflow runtime for AI coding agents.
|
|
4
|
+
|
|
5
|
+
Start here:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
xflow guide
|
|
9
|
+
xflow assess
|
|
10
|
+
xflow demo clean
|
|
11
|
+
xflow demo launch
|
|
12
|
+
xflow launch audit
|
|
13
|
+
xflow adoption kit --name first-team --source first-tracker --track yolo
|
|
14
|
+
xflow adoption init --name first-team --source first-tracker --track yolo
|
|
15
|
+
xflow adoption validate --json
|
|
16
|
+
xflow package audit --check-registry --json
|
|
17
|
+
xflow init --project-root .
|
|
18
|
+
xflow goal set "Ship the next verified change" --project-root .
|
|
19
|
+
xflow goal audit --project-root . --json
|
|
20
|
+
xflow doctor --project-root .
|
|
21
|
+
xflow workflow validate yolo --project-root .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Core entry points:
|
|
25
|
+
|
|
26
|
+
- `xflow:takein` - load `HANDOFF.md`, `AHA.md`, and local git state before acting.
|
|
27
|
+
- `xflow:goal` - set, inspect, or audit the project direction anchor in `.xflow/GOAL.md`; CLI users can run `xflow goal set` and `xflow goal audit`.
|
|
28
|
+
- `xflow:plan` - track-neutral planning only; produces `plan.md`.
|
|
29
|
+
- `xflow:ralph` - conversation-native Ralph/autopilot loop for verified completion and feedback repair.
|
|
30
|
+
- `xflow:yolo` - lite execution track for backend, docs, infra, and small UI changes.
|
|
31
|
+
- `xflow:corps` - heavy execution track for product/UI and multi-agent delivery.
|
|
32
|
+
|
|
33
|
+
Supporting entry points:
|
|
34
|
+
|
|
35
|
+
- `xflow:handoff` - refresh project handoff context.
|
|
36
|
+
- `xflow:lookback` - re-check durable project context before acting.
|
|
37
|
+
- `xflow:aha` - append durable insights to `AHA.md`.
|
|
38
|
+
|
|
39
|
+
Adjacent project:
|
|
40
|
+
|
|
41
|
+
- `xmem` - separate shared-memory skill family, now hosted in `as-xmem`.
|
|
42
|
+
|
|
43
|
+
Public docs:
|
|
44
|
+
|
|
45
|
+
- `README.md`
|
|
46
|
+
- `docs/quickstart.md`
|
|
47
|
+
- `docs/corps-operator-guide.md`
|
|
48
|
+
- `docs/install-upgrade.md`
|
|
49
|
+
- `docs/tooling-matrix.md`
|
|
50
|
+
- `docs/demo-proof.md`
|
|
51
|
+
- `docs/examples-gallery.md`
|
|
52
|
+
- `docs/public-benchmark.md`
|
|
53
|
+
- `docs/quality-assessment.md`
|
|
54
|
+
- `docs/launch-demo.md`
|
|
55
|
+
- `docs/launch-dossier.md`
|
|
56
|
+
- `docs/adoption/README.md`
|
|
57
|
+
- `docs/goal-vs-codex.md`
|
|
58
|
+
- `docs/walkthrough.md`
|
|
59
|
+
- `docs/openspec-migration.md`
|
|
60
|
+
- `docs/spec-kit-benchmark.md`
|
|
61
|
+
- `docs/integrations.md`
|
|
62
|
+
- `docs/public-release.md`
|
|
63
|
+
- `RELEASE_NOTES.md`
|
|
64
|
+
- `docs/fixtures/tracker-item.json`
|
|
65
|
+
- `docs/compatibility.md`
|
|
66
|
+
- `docs/methodology.md`
|
|
67
|
+
- `docs/reviewer-guide.md`
|
|
68
|
+
- `docs/team-adoption.md`
|
|
69
|
+
- `docs/competitive-benchmark.md`
|
|
70
|
+
|
|
71
|
+
Executable sources:
|
|
72
|
+
|
|
73
|
+
- `workflows/yolo.yaml`
|
|
74
|
+
- `workflows/corps.yaml`
|
|
75
|
+
- `atoms/registry.json`
|
|
76
|
+
- `specs/workflow.md`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xflow:aha
|
|
3
|
+
description: 将高价值讨论洞察、设计认知、流程经验和具持续影响的转折点沉淀到项目根目录的规范 `AHA.md` 中。适用于当前线程产生了会影响后续执行、评审、设计或流程决策的重要认识时。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Superflow Aha
|
|
7
|
+
|
|
8
|
+
Write durable insight into exactly one canonical file:
|
|
9
|
+
|
|
10
|
+
- project root `AHA.md`
|
|
11
|
+
|
|
12
|
+
`AHA.md` is not a handoff replacement.
|
|
13
|
+
It is the canonical log of high-signal realizations that would be costly to rediscover.
|
|
14
|
+
|
|
15
|
+
## When to record
|
|
16
|
+
|
|
17
|
+
Record an aha when any of these become true:
|
|
18
|
+
|
|
19
|
+
- a root cause reframes the problem
|
|
20
|
+
- a workflow weakness is identified clearly
|
|
21
|
+
- a design/system principle is discovered that should constrain future work
|
|
22
|
+
- a repeated failure mode becomes understandable
|
|
23
|
+
- a strong product or design insight should influence later implementation and review
|
|
24
|
+
|
|
25
|
+
Do not log routine status updates, generic summaries, or low-signal chatter.
|
|
26
|
+
|
|
27
|
+
## Required structure
|
|
28
|
+
|
|
29
|
+
Each entry in `AHA.md` should include:
|
|
30
|
+
|
|
31
|
+
1. date
|
|
32
|
+
2. title
|
|
33
|
+
3. insight
|
|
34
|
+
4. why it matters
|
|
35
|
+
5. consequence for workflow / design / execution
|
|
36
|
+
6. evidence anchors
|
|
37
|
+
|
|
38
|
+
Keep entries compact and forceful.
|
|
39
|
+
|
|
40
|
+
## Workflow
|
|
41
|
+
|
|
42
|
+
1. Inspect the current repo state and discussion context.
|
|
43
|
+
2. Read existing `AHA.md` if present.
|
|
44
|
+
3. Merge with the existing log instead of scattering similar insights.
|
|
45
|
+
4. Add the new aha as a durable entry with anchors.
|
|
46
|
+
5. If the insight changes operational expectations, mention which skills, docs, or gates should consume it.
|
|
47
|
+
|
|
48
|
+
## Guardrails
|
|
49
|
+
|
|
50
|
+
- `AHA.md` is for durable realizations, not chronological journaling.
|
|
51
|
+
- Prefer one strong entry over several weak entries.
|
|
52
|
+
- Link the insight to exact files or contracts when possible.
|
|
53
|
+
- If an insight becomes obsolete, mark it superseded rather than silently deleting history.
|
|
54
|
+
|
|
55
|
+
## Suggested format
|
|
56
|
+
|
|
57
|
+
Use this structure unless the project already has a stronger convention:
|
|
58
|
+
|
|
59
|
+
- title with project name
|
|
60
|
+
- `## Active Ahas`
|
|
61
|
+
- repeated `### YYYY-MM-DD — Title`
|
|
62
|
+
- short bullets:
|
|
63
|
+
- `Insight`
|
|
64
|
+
- `Why It Matters`
|
|
65
|
+
- `Action`
|
|
66
|
+
- `Evidence`
|
|
67
|
+
|
|
68
|
+
## Script
|
|
69
|
+
|
|
70
|
+
The agent autonomously calls this helper to append structured aha entries:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python3 ~/.codex/skills/xflow/aha/scripts/append_aha.py \
|
|
74
|
+
--title "Title" \
|
|
75
|
+
--insight "Core insight" \
|
|
76
|
+
--why "Why it matters" \
|
|
77
|
+
--action "Required action" \
|
|
78
|
+
--evidence "/abs/path/or/doc"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Agent determines parameters from discussion context.
|
|
82
|
+
User invokes skill, agent handles execution.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_args() -> argparse.Namespace:
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
description="Append a structured aha entry to the canonical root AHA.md.",
|
|
10
|
+
)
|
|
11
|
+
parser.add_argument("--project-root", default=".", help="Project root containing AHA.md.")
|
|
12
|
+
parser.add_argument("--title", required=True, help="Short aha title.")
|
|
13
|
+
parser.add_argument("--insight", required=True, help="Core insight.")
|
|
14
|
+
parser.add_argument("--why", required=True, help="Why this matters.")
|
|
15
|
+
parser.add_argument("--action", required=True, help="Required workflow/design action.")
|
|
16
|
+
parser.add_argument("--evidence", action="append", default=[], help="Evidence anchor. Can be repeated.")
|
|
17
|
+
return parser.parse_args()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ensure_header(path: Path) -> None:
|
|
21
|
+
if path.exists():
|
|
22
|
+
return
|
|
23
|
+
path.write_text("# AHA\n\n## Active Ahas\n", encoding="utf-8")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> int:
|
|
27
|
+
args = parse_args()
|
|
28
|
+
root = Path(args.project_root).expanduser().resolve()
|
|
29
|
+
aha_path = root / "AHA.md"
|
|
30
|
+
ensure_header(aha_path)
|
|
31
|
+
|
|
32
|
+
entry_lines = [
|
|
33
|
+
"",
|
|
34
|
+
f"### {date.today().isoformat()} — {args.title.strip()}",
|
|
35
|
+
f"- Insight: {args.insight.strip()}",
|
|
36
|
+
f"- Why It Matters: {args.why.strip()}",
|
|
37
|
+
f"- Action: {args.action.strip()}",
|
|
38
|
+
]
|
|
39
|
+
if args.evidence:
|
|
40
|
+
entry_lines.append("- Evidence:")
|
|
41
|
+
for item in args.evidence:
|
|
42
|
+
entry_lines.append(f" - {item.strip()}")
|
|
43
|
+
|
|
44
|
+
with aha_path.open("a", encoding="utf-8") as handle:
|
|
45
|
+
handle.write("\n".join(entry_lines) + "\n")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
sys.path.append(str(Path(__file__).resolve().parents[2] / "scripts"))
|
|
10
|
+
|
|
11
|
+
from doctor import run_doctor_checks # noqa: E402
|
|
12
|
+
from common import assert_stage_transition_allowed, assert_status_transition_allowed, find_git_root, get_current_branch, get_remote_url, load_status, maybe_relative, now_iso, record_stage_transition, record_value_transition, resolve_project_context, run, save_status, set_project_single_select_status, upsert_issue_comment # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
TARGETS = {
|
|
16
|
+
"product": "product.md",
|
|
17
|
+
"architecture": "architecture.md",
|
|
18
|
+
"workflow": "workflow.md",
|
|
19
|
+
"ux": "ux.md",
|
|
20
|
+
"design": "DESIGN.md",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_args() -> argparse.Namespace:
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
description="Conservatively archive a Superflow change into root specs.",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--project-root",
|
|
30
|
+
default=".",
|
|
31
|
+
help="Project root containing specs/.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--change-id",
|
|
35
|
+
required=True,
|
|
36
|
+
help="Change identifier under specs/changes/.",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument("--repo", help="Override target repository for issue/project close-out.")
|
|
39
|
+
parser.add_argument("--issue-number", type=int, help="Override linked issue number for close-out.")
|
|
40
|
+
parser.add_argument("--checkout-path", help="Override local checkout path for git commit/push.")
|
|
41
|
+
parser.add_argument("--commit-message", help="Commit message to use for archive publishing.")
|
|
42
|
+
parser.add_argument("--push", action="store_true", help="Push the current branch after commit.")
|
|
43
|
+
parser.add_argument("--allow-detached-branch-publish", action="store_true", help="Allow commit/push even when the current branch does not match the linked issue branch.")
|
|
44
|
+
parser.add_argument("--skip-doctor-precheck", action="store_true", help="Skip the automatic doctor precheck before archive publish.")
|
|
45
|
+
parser.add_argument("--close-issue", action="store_true", help="Close the linked issue after archive.")
|
|
46
|
+
parser.add_argument("--project-owner", help="GitHub project owner login for project status updates.")
|
|
47
|
+
parser.add_argument("--project-number", type=int, help="GitHub project number for project status updates.")
|
|
48
|
+
parser.add_argument("--status-field-name", default="Status", help="Project single select field name.")
|
|
49
|
+
parser.add_argument("--done-option-name", default="Done", help="Project status option name to apply.")
|
|
50
|
+
parser.add_argument("--merge-snippets-only", action="store_true", help="Only merge change-scoped merge-*.md snippets into root specs.")
|
|
51
|
+
return parser.parse_args()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_text(path: Path) -> str:
|
|
55
|
+
return path.read_text(encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_text(path: Path, content: str) -> None:
|
|
59
|
+
path.write_text(content, encoding="utf-8")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def stage_archive_paths(project_root: Path, specs_root: Path, change_root: Path) -> None:
|
|
63
|
+
candidates = [
|
|
64
|
+
specs_root,
|
|
65
|
+
project_root / "HANDOFF.md",
|
|
66
|
+
project_root / "AHA.md",
|
|
67
|
+
project_root / "DESIGN.md",
|
|
68
|
+
]
|
|
69
|
+
existing = [str(path) for path in candidates if path.exists()]
|
|
70
|
+
if existing:
|
|
71
|
+
run(["git", "add", *existing], cwd=project_root)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_unstaged_tracked_changes(project_root: Path) -> list[str]:
|
|
75
|
+
result = subprocess.run(
|
|
76
|
+
["git", "diff", "--name-only"],
|
|
77
|
+
cwd=str(project_root),
|
|
78
|
+
text=True,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
check=True,
|
|
81
|
+
)
|
|
82
|
+
return [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def assert_no_unstaged_tracked_changes(project_root: Path) -> None:
|
|
86
|
+
paths = list_unstaged_tracked_changes(project_root)
|
|
87
|
+
if not paths:
|
|
88
|
+
return
|
|
89
|
+
joined = "\n- ".join(paths)
|
|
90
|
+
raise SystemExit(
|
|
91
|
+
"Refusing archive: unstaged tracked changes remain after staging archive paths. "
|
|
92
|
+
"Stage intended implementation/test changes before archive, or clean unrelated edits:\n"
|
|
93
|
+
f"- {joined}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def refresh_handoff_current_state(project_root: Path, change_id: str) -> None:
|
|
98
|
+
script = Path(__file__).resolve().parents[2] / "handoff" / "scripts" / "scaffold_handoff.py"
|
|
99
|
+
if script.exists():
|
|
100
|
+
run([
|
|
101
|
+
sys.executable,
|
|
102
|
+
str(script),
|
|
103
|
+
"--project-root",
|
|
104
|
+
str(project_root),
|
|
105
|
+
"--refresh",
|
|
106
|
+
"--change-id",
|
|
107
|
+
change_id,
|
|
108
|
+
], cwd=project_root)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def has_staged_changes(project_root: Path) -> bool:
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
["git", "diff", "--cached", "--quiet"],
|
|
114
|
+
cwd=str(project_root),
|
|
115
|
+
text=True,
|
|
116
|
+
capture_output=True,
|
|
117
|
+
)
|
|
118
|
+
return result.returncode == 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def append_change_block(target_path: Path, change_id: str, snippet_path: Path) -> bool:
|
|
122
|
+
snippet = read_text(snippet_path).strip()
|
|
123
|
+
if not snippet:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
existing = read_text(target_path) if target_path.exists() else ""
|
|
127
|
+
marker = f"## Change: {change_id}"
|
|
128
|
+
if marker in existing:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
timestamp = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
132
|
+
block = (
|
|
133
|
+
f"\n\n{marker}\n\n"
|
|
134
|
+
f"Source: `{snippet_path}`\n"
|
|
135
|
+
f"Archived: {timestamp}\n\n"
|
|
136
|
+
f"{snippet}\n"
|
|
137
|
+
)
|
|
138
|
+
if existing and not existing.endswith("\n"):
|
|
139
|
+
existing += "\n"
|
|
140
|
+
write_text(target_path, existing + block)
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main() -> int:
|
|
145
|
+
args = parse_args()
|
|
146
|
+
project_root = Path(args.project_root).expanduser().resolve()
|
|
147
|
+
specs_root = project_root / "specs"
|
|
148
|
+
change_root = specs_root / "changes" / args.change_id
|
|
149
|
+
|
|
150
|
+
required_paths = [
|
|
151
|
+
change_root / "proposal.md",
|
|
152
|
+
change_root / "plan.md",
|
|
153
|
+
change_root / "tasks.md",
|
|
154
|
+
change_root / "status.json",
|
|
155
|
+
]
|
|
156
|
+
missing = [str(path) for path in required_paths if not path.exists()]
|
|
157
|
+
if missing:
|
|
158
|
+
raise SystemExit("Missing required change artifacts:\n- " + "\n- ".join(missing))
|
|
159
|
+
|
|
160
|
+
status_path, status = load_status(project_root, args.change_id)
|
|
161
|
+
|
|
162
|
+
if not args.merge_snippets_only and status.get("current_stage") not in {"verify", "archive"} and status.get("status") not in {"done", "blocked"}:
|
|
163
|
+
raise SystemExit("Refusing archive: change still appears in flight.")
|
|
164
|
+
|
|
165
|
+
verification_status = status.get("verification_status")
|
|
166
|
+
if not args.merge_snippets_only and status.get("status") == "done" and verification_status != "passed":
|
|
167
|
+
raise SystemExit("Refusing archive: completed change must have verification_status=passed.")
|
|
168
|
+
|
|
169
|
+
# Check design alignment for frontend changes
|
|
170
|
+
change_type = status.get("change_type", "backend")
|
|
171
|
+
design_aligned = status.get("design_aligned", "na")
|
|
172
|
+
if not args.merge_snippets_only and change_type in ("frontend", "full-stack") and design_aligned not in ("yes", "na"):
|
|
173
|
+
if design_aligned == "no":
|
|
174
|
+
raise SystemExit("Refusing archive: frontend change has design_aligned=no. Resolve design drift first.")
|
|
175
|
+
elif design_aligned == "pending":
|
|
176
|
+
print("Warning: frontend change has design_aligned=pending. Proceeding but recommend design verification.")
|
|
177
|
+
|
|
178
|
+
if not args.merge_snippets_only and status.get("archival_status") == "archived":
|
|
179
|
+
raise SystemExit("Refusing archive: change is already archived.")
|
|
180
|
+
|
|
181
|
+
if not args.merge_snippets_only:
|
|
182
|
+
stage_archive_paths(project_root, specs_root, change_root)
|
|
183
|
+
assert_no_unstaged_tracked_changes(project_root)
|
|
184
|
+
|
|
185
|
+
updated_targets: list[str] = []
|
|
186
|
+
for key, filename in TARGETS.items():
|
|
187
|
+
snippet_path = change_root / f"merge-{key}.md"
|
|
188
|
+
if not snippet_path.exists():
|
|
189
|
+
continue
|
|
190
|
+
# DESIGN.md is at project root, others are in specs/
|
|
191
|
+
if key == "design":
|
|
192
|
+
target_path = project_root / filename
|
|
193
|
+
else:
|
|
194
|
+
target_path = specs_root / filename
|
|
195
|
+
if not target_path.exists():
|
|
196
|
+
# For design, create if snippet exists and frontend change
|
|
197
|
+
if key == "design" and change_type in ("frontend", "full-stack"):
|
|
198
|
+
# Copy template
|
|
199
|
+
template_path = Path(__file__).resolve().parents[2] / "yolo" / "templates" / "DESIGN.md"
|
|
200
|
+
if template_path.exists():
|
|
201
|
+
write_text(target_path, read_text(template_path))
|
|
202
|
+
else:
|
|
203
|
+
raise SystemExit(f"Missing DESIGN.md template at {template_path}")
|
|
204
|
+
else:
|
|
205
|
+
raise SystemExit(f"Missing target spec file for merge: {target_path}")
|
|
206
|
+
changed = append_change_block(target_path, args.change_id, snippet_path)
|
|
207
|
+
if changed:
|
|
208
|
+
updated_targets.append(str(target_path))
|
|
209
|
+
|
|
210
|
+
if args.merge_snippets_only:
|
|
211
|
+
print("Mode: merge-snippets-only")
|
|
212
|
+
print(f"Merged snippets for change: {args.change_id}")
|
|
213
|
+
if updated_targets:
|
|
214
|
+
print("Updated root specs:")
|
|
215
|
+
for target in updated_targets:
|
|
216
|
+
print(f"- {target}")
|
|
217
|
+
else:
|
|
218
|
+
print("No root specs updated: no merge-*.md snippets were present.")
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
assert_stage_transition_allowed(
|
|
222
|
+
status.get("current_stage"),
|
|
223
|
+
"archive",
|
|
224
|
+
current_status=status.get("status"),
|
|
225
|
+
allow_terminal_status_override=("done", "blocked"),
|
|
226
|
+
)
|
|
227
|
+
if status.get("status") == "done":
|
|
228
|
+
assert_status_transition_allowed(status.get("status"), "done", allow_terminal_status_override=("done",))
|
|
229
|
+
elif status.get("status") == "blocked":
|
|
230
|
+
assert_status_transition_allowed(status.get("status"), "blocked", allow_terminal_status_override=("blocked",))
|
|
231
|
+
else:
|
|
232
|
+
assert_status_transition_allowed(status.get("status"), "done")
|
|
233
|
+
record_stage_transition(status, "archive", reason="archive script began canonical close-out")
|
|
234
|
+
if status.get("status") != "blocked":
|
|
235
|
+
record_value_transition(status, "status", "done", reason="archive completed canonical close-out")
|
|
236
|
+
status["archival_status"] = "archived"
|
|
237
|
+
status["next_action"] = "Archive complete. Refresh HANDOFF.md and durable lessons if new information emerged."
|
|
238
|
+
save_status(status_path, status)
|
|
239
|
+
refresh_handoff_current_state(project_root, args.change_id)
|
|
240
|
+
|
|
241
|
+
repo = args.repo or status.get("issue_repo") or status.get("target_repo")
|
|
242
|
+
issue_number = args.issue_number or status.get("issue_number")
|
|
243
|
+
checkout_path = Path(args.checkout_path or status.get("checkout_path") or find_git_root(project_root))
|
|
244
|
+
linked_branch = status.get("branch_name")
|
|
245
|
+
commit_message = args.commit_message or f"archive(xflow): close {args.change_id}"
|
|
246
|
+
|
|
247
|
+
if repo and issue_number and linked_branch and not args.skip_doctor_precheck:
|
|
248
|
+
pre_result = run_doctor_checks(
|
|
249
|
+
project_root=project_root,
|
|
250
|
+
change_id=args.change_id,
|
|
251
|
+
repo=repo,
|
|
252
|
+
issue_number=int(issue_number),
|
|
253
|
+
branch_name=linked_branch,
|
|
254
|
+
checkout_path_override=checkout_path,
|
|
255
|
+
)
|
|
256
|
+
if not pre_result["ok"]:
|
|
257
|
+
raise SystemExit("Doctor precheck failed before archive publish. Run doctor.py for details.")
|
|
258
|
+
|
|
259
|
+
remote_url = None
|
|
260
|
+
if checkout_path:
|
|
261
|
+
remote_url = get_remote_url(checkout_path)
|
|
262
|
+
|
|
263
|
+
if linked_branch and not args.allow_detached_branch_publish:
|
|
264
|
+
current_branch = get_current_branch(checkout_path)
|
|
265
|
+
if current_branch != linked_branch:
|
|
266
|
+
raise SystemExit(
|
|
267
|
+
f"Refusing publish: current branch is {current_branch!r}, but linked issue branch is {linked_branch!r}. "
|
|
268
|
+
"Switch to the linked branch or pass --allow-detached-branch-publish."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
stage_archive_paths(project_root, specs_root, change_root)
|
|
272
|
+
commit_result = "no staged changes"
|
|
273
|
+
if has_staged_changes(project_root):
|
|
274
|
+
run(["git", "commit", "-m", commit_message], cwd=checkout_path)
|
|
275
|
+
commit_result = commit_message
|
|
276
|
+
|
|
277
|
+
push_result = "skipped"
|
|
278
|
+
if args.push:
|
|
279
|
+
run(["git", "push"], cwd=checkout_path)
|
|
280
|
+
push_result = "pushed"
|
|
281
|
+
|
|
282
|
+
project_context = resolve_project_context(repo, int(issue_number), args.project_owner, args.project_number) if repo and issue_number else None
|
|
283
|
+
project_result = None
|
|
284
|
+
if repo and issue_number and project_context:
|
|
285
|
+
project_result = set_project_single_select_status(
|
|
286
|
+
project_context["owner"],
|
|
287
|
+
int(project_context["number"]),
|
|
288
|
+
args.status_field_name,
|
|
289
|
+
args.done_option_name,
|
|
290
|
+
repo,
|
|
291
|
+
int(issue_number),
|
|
292
|
+
)
|
|
293
|
+
elif repo and issue_number:
|
|
294
|
+
upsert_issue_comment(
|
|
295
|
+
repo,
|
|
296
|
+
int(issue_number),
|
|
297
|
+
"<!-- xflow:archive-state -->",
|
|
298
|
+
"Archive completed. No GitHub Project context was provided, so Superflow recorded completion via issue comment fallback.",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if repo and issue_number and args.close_issue:
|
|
302
|
+
run(["gh", "issue", "close", str(issue_number), "--repo", repo], cwd=checkout_path)
|
|
303
|
+
|
|
304
|
+
status["git_publish"] = {
|
|
305
|
+
"commit": commit_result,
|
|
306
|
+
"push": push_result,
|
|
307
|
+
"remote_url": remote_url,
|
|
308
|
+
}
|
|
309
|
+
if project_result:
|
|
310
|
+
status["project_done"] = {
|
|
311
|
+
"owner": project_context["owner"],
|
|
312
|
+
"number": int(project_context["number"]),
|
|
313
|
+
"field_name": args.status_field_name,
|
|
314
|
+
"option_name": args.done_option_name,
|
|
315
|
+
"source": project_context.get("source", "explicit"),
|
|
316
|
+
**project_result,
|
|
317
|
+
}
|
|
318
|
+
save_status(status_path, status)
|
|
319
|
+
|
|
320
|
+
print("Mode: full-publish")
|
|
321
|
+
print(f"Archived change: {args.change_id}")
|
|
322
|
+
if updated_targets:
|
|
323
|
+
print("Updated root specs:")
|
|
324
|
+
for target in updated_targets:
|
|
325
|
+
print(f"- {target}")
|
|
326
|
+
else:
|
|
327
|
+
print("No root specs updated: no merge-*.md snippets were present.")
|
|
328
|
+
print(f"Commit: {commit_result}")
|
|
329
|
+
print(f"Push: {push_result}")
|
|
330
|
+
if project_result:
|
|
331
|
+
print(f"Project status: {args.done_option_name}")
|
|
332
|
+
elif repo and issue_number:
|
|
333
|
+
print("Project status: issue comment fallback")
|
|
334
|
+
return 0
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Design Merge Snippet
|
|
2
|
+
|
|
3
|
+
> This file contains design decisions to merge into `DESIGN.md` or `specs/ux.md` during archive.
|
|
4
|
+
> Place in `specs/changes/<change-id>/merge-design.md`.
|
|
5
|
+
|
|
6
|
+
## Change Summary
|
|
7
|
+
|
|
8
|
+
- Change ID: [change-id]
|
|
9
|
+
- Date: [YYYY-MM-DD]
|
|
10
|
+
- Title: [change title]
|
|
11
|
+
|
|
12
|
+
## Design Decisions
|
|
13
|
+
|
|
14
|
+
### Visual Tone
|
|
15
|
+
- [decision made, if changed]
|
|
16
|
+
- [reasoning]
|
|
17
|
+
|
|
18
|
+
### Color System
|
|
19
|
+
- [colors added/changed]
|
|
20
|
+
- [reasoning]
|
|
21
|
+
|
|
22
|
+
### Typography
|
|
23
|
+
- [fonts added/changed]
|
|
24
|
+
- [reasoning]
|
|
25
|
+
|
|
26
|
+
### Layout & Spacing
|
|
27
|
+
- [spacing changes]
|
|
28
|
+
- [reasoning]
|
|
29
|
+
|
|
30
|
+
### Motion
|
|
31
|
+
- [animation changes]
|
|
32
|
+
- [reasoning]
|
|
33
|
+
|
|
34
|
+
### Component Patterns
|
|
35
|
+
- [new patterns added]
|
|
36
|
+
- [reasoning]
|
|
37
|
+
|
|
38
|
+
## Design Decisions Log Entry
|
|
39
|
+
|
|
40
|
+
Add this to the Design Decisions Log section:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
- [YYYY-MM-DD] [change-id]: [key design decision] — [reasoning]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Verification
|
|
47
|
+
|
|
48
|
+
- Design alignment verified: [yes/no]
|
|
49
|
+
- Visual comparison done: [yes/no]
|
|
50
|
+
- Matches DESIGN.md specifications: [yes/no/partial]
|
|
51
|
+
|
|
52
|
+
## Notes
|
|
53
|
+
|
|
54
|
+
[Any additional design-related notes for future reference]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Workflow Merge Snippet
|
|
2
|
+
|
|
3
|
+
> This file contains workflow or capability deltas to merge into `specs/workflow.md` during archive.
|
|
4
|
+
> Place in `specs/changes/<change-id>/merge-workflow.md`.
|
|
5
|
+
|
|
6
|
+
## Change Summary
|
|
7
|
+
|
|
8
|
+
- Change ID: [change-id]
|
|
9
|
+
- Date: [YYYY-MM-DD]
|
|
10
|
+
- Title: [change title]
|
|
11
|
+
|
|
12
|
+
## Capability Delta
|
|
13
|
+
|
|
14
|
+
- [what capability or boundary changed]
|
|
15
|
+
|
|
16
|
+
## Behavior Changes
|
|
17
|
+
|
|
18
|
+
- [what behavior changed]
|
|
19
|
+
|
|
20
|
+
## Constraints and Invariants
|
|
21
|
+
|
|
22
|
+
- [what remains true]
|
|
23
|
+
|
|
24
|
+
## Migration Notes
|
|
25
|
+
|
|
26
|
+
- [any follow-up required to keep workflow/specs aligned]
|