@devcxl/opencode-cabbage 1.2.0 → 2.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.
- package/.codex-plugin/plugin.json +36 -0
- package/README.md +67 -21
- package/README.zh.md +26 -13
- package/assets/agents/dev-lifecycle.md +6 -0
- package/assets/prompts/bootstrap.md +4 -0
- package/assets/skills/flow-change/SKILL.md +109 -0
- package/assets/skills/flow-design/SKILL.md +15 -0
- package/assets/skills/flow-docs/SKILL.md +112 -0
- package/assets/skills/flow-incident/SKILL.md +98 -0
- package/assets/skills/flow-research/SKILL.md +9 -0
- package/assets/skills/flow-setup/SKILL.md +13 -0
- package/codex-skills/agent-architect/SKILL.md +79 -0
- package/codex-skills/agent-dev-lifecycle/SKILL.md +311 -0
- package/codex-skills/agent-developer/SKILL.md +51 -0
- package/codex-skills/agent-goal-verify/SKILL.md +45 -0
- package/codex-skills/agent-researcher/SKILL.md +32 -0
- package/codex-skills/agent-reviewer/SKILL.md +109 -0
- package/codex-skills/flow-change/SKILL.md +109 -0
- package/codex-skills/flow-code/SKILL.md +71 -0
- package/codex-skills/flow-design/SKILL.md +161 -0
- package/codex-skills/flow-docs/SKILL.md +112 -0
- package/codex-skills/flow-incident/SKILL.md +98 -0
- package/codex-skills/flow-release/SKILL.md +90 -0
- package/codex-skills/flow-requirements/SKILL.md +90 -0
- package/codex-skills/flow-research/SKILL.md +86 -0
- package/codex-skills/flow-review/SKILL.md +110 -0
- package/codex-skills/flow-setup/SKILL.md +88 -0
- package/codex-skills/flow-tasks/SKILL.md +220 -0
- package/codex-skills/flow-tdd/SKILL.md +235 -0
- package/dist/hooks/session-start.d.ts +1 -0
- package/dist/hooks/session-start.js +72 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/plugin/agents.d.ts +20 -0
- package/dist/plugin/agents.js +79 -0
- package/dist/plugin/agents.js.map +1 -1
- package/dist/plugin/cabbage-cli.d.ts +28 -0
- package/dist/plugin/cabbage-cli.js +92 -0
- package/dist/plugin/cabbage-cli.js.map +1 -0
- package/dist/plugin/cabbage-tool.d.ts +103 -0
- package/dist/plugin/cabbage-tool.js +169 -0
- package/dist/plugin/cabbage-tool.js.map +1 -0
- package/dist/plugin/commands.d.ts +10 -0
- package/dist/plugin/commands.js +1 -1
- package/dist/plugin/commands.js.map +1 -1
- package/dist/plugin/continuation.d.ts +22 -0
- package/dist/plugin/continuation.js +233 -0
- package/dist/plugin/continuation.js.map +1 -0
- package/dist/plugin/goal-store.d.ts +64 -0
- package/dist/plugin/goal-store.js +57 -0
- package/dist/plugin/goal-store.js.map +1 -0
- package/dist/plugin/goal.d.ts +95 -38
- package/dist/plugin/goal.js +208 -199
- package/dist/plugin/goal.js.map +1 -1
- package/dist/plugin/install-dir.d.ts +13 -0
- package/dist/plugin/install-dir.js +46 -0
- package/dist/plugin/install-dir.js.map +1 -0
- package/dist/plugin/server.d.ts +4 -12
- package/dist/plugin/server.js +279 -423
- package/dist/plugin/server.js.map +1 -1
- package/dist/plugin/session-abort.d.ts +5 -0
- package/dist/plugin/session-abort.js +26 -0
- package/dist/plugin/session-abort.js.map +1 -0
- package/dist/plugin/skills.d.ts +15 -0
- package/dist/plugin/skills.js +47 -50
- package/dist/plugin/skills.js.map +1 -1
- package/dist/plugin.d.ts +2 -2
- package/dist/plugin.js +6 -2
- package/dist/plugin.js.map +1 -1
- package/dist/util/paths.d.ts +0 -6
- package/dist/util/paths.js +0 -15
- package/dist/util/paths.js.map +1 -1
- package/hooks/hooks.json +17 -0
- package/package.json +13 -8
- package/vendor/cabbage/MANIFEST.json +8 -0
- package/vendor/cabbage/cabbage_cli/__init__.py +1 -0
- package/vendor/cabbage/cabbage_cli/__main__.py +2 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/.vitepress/config.ts +78 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/README.md +31 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/package.json +16 -0
- package/vendor/cabbage/cabbage_cli/assets/docs-site/pnpm-lock.yaml +2504 -0
- package/vendor/cabbage/cabbage_cli/assets/integrations/cabbage.yml +127 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/adr.md +47 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/api-design.md +45 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/change-record.md +21 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/database-design.md +37 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/generic.md +20 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/impact.md +41 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/incident.md +48 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/postmortem.md +43 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/prd.md +71 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/release-plan.md +45 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/rfc.md +44 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/security-review.md +35 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/tasks.md +62 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/tech-spec.md +87 -0
- package/vendor/cabbage/cabbage_cli/assets/templates/test-plan.md +44 -0
- package/vendor/cabbage/cabbage_cli/assets/workflows/change.yaml +11 -0
- package/vendor/cabbage/cabbage_cli/assets/workflows/incident.yaml +4 -0
- package/vendor/cabbage/cabbage_cli/cli.py +283 -0
- package/vendor/cabbage/cabbage_cli/core.py +606 -0
- package/vendor/cabbage/cabbage_cli/scaffold.py +313 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
change: {{CHANGE_ID}}
|
|
3
|
+
cabbage_stage: {{STAGE_ID}}
|
|
4
|
+
change_type: {{CHANGE_TYPE}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Replace every marked prompt before verifying this stage. Split work into vertical tracer-bullet tasks with an explicit DAG dependency graph and complete every checkbox. -->
|
|
8
|
+
|
|
9
|
+
# Preparation
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart TD
|
|
13
|
+
Pre["Prerequisites / Pre-Refactor"] --> Task1["Task 1: Core Behavior Slice"]
|
|
14
|
+
Task1 --> Task2["Task 2: Parallel Behavior Slice A"]
|
|
15
|
+
Task1 --> Task3["Task 3: Parallel Behavior Slice B"]
|
|
16
|
+
Task2 --> Final["Convergence & Sync"]
|
|
17
|
+
Task3 --> Final
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Parallel Execution Strategy
|
|
21
|
+
|
|
22
|
+
- **Sequential Baseline**: Prerequisites, data models, or behavior-preserving pre-refactors.
|
|
23
|
+
- **Parallel Slices**: Independent vertical feature slices executing concurrently in isolated contexts.
|
|
24
|
+
- **Convergence & Verification**: Integration tests, docs synchronization, and release checks.
|
|
25
|
+
|
|
26
|
+
- [ ] <!-- CABBAGE: Record prerequisite verification, baseline fixtures, or behavior-preserving pre-refactors. -->
|
|
27
|
+
|
|
28
|
+
# Tasks
|
|
29
|
+
|
|
30
|
+
## Task 1: <!-- CABBAGE: Primary Feature Slice Slug -->
|
|
31
|
+
- **Builds**: <!-- CABBAGE: Observable end-to-end user/system capability delivered by this task. -->
|
|
32
|
+
- **Blocked By**: None (or Preparation)
|
|
33
|
+
- **Parallel Group**: Group 1
|
|
34
|
+
- **Verification**: `<!-- CABBAGE: targeted test command -->`
|
|
35
|
+
- **Standard Operating Procedure (Task SOP)**:
|
|
36
|
+
1. *[RED]* Define public test seam and write failing test.
|
|
37
|
+
2. *[GREEN]* Implement minimal code to pass the test.
|
|
38
|
+
3. *[REFACTOR]* Clean code, verify lint/types, maintain behavior.
|
|
39
|
+
4. *[VERIFY]* Run verification command and confirm passing.
|
|
40
|
+
- [ ] <!-- CABBAGE: Implement behavior slice 1 through public test seam. -->
|
|
41
|
+
- [ ] <!-- CABBAGE: Add failing test (RED) then minimal implementation (GREEN). -->
|
|
42
|
+
|
|
43
|
+
## Task 2: <!-- CABBAGE: Secondary Feature / Extension Slice Slug -->
|
|
44
|
+
- **Builds**: <!-- CABBAGE: Observable end-to-end capability delivered by this task. -->
|
|
45
|
+
- **Blocked By**: Task 1
|
|
46
|
+
- **Parallel Group**: Group 2
|
|
47
|
+
- **Verification**: `<!-- CABBAGE: targeted test command -->`
|
|
48
|
+
- **Standard Operating Procedure (Task SOP)**:
|
|
49
|
+
1. *[RED]* Define public test seam and write failing test.
|
|
50
|
+
2. *[GREEN]* Implement minimal code to pass the test.
|
|
51
|
+
3. *[REFACTOR]* Clean code, verify lint/types, maintain behavior.
|
|
52
|
+
4. *[VERIFY]* Run verification command and confirm passing.
|
|
53
|
+
- [ ] <!-- CABBAGE: Implement behavior slice 2 through public test seam. -->
|
|
54
|
+
- [ ] <!-- CABBAGE: Add failing test (RED) then minimal implementation (GREEN). -->
|
|
55
|
+
|
|
56
|
+
- [ ] Add or update automated regression tests
|
|
57
|
+
- [ ] Update affected current-state documentation under docs/
|
|
58
|
+
|
|
59
|
+
# Verification
|
|
60
|
+
|
|
61
|
+
- [ ] <!-- CABBAGE: Record the exact test, validation, and build commands to run. -->
|
|
62
|
+
- [ ] Verify rollout and rollback readiness when applicable
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
change: {{CHANGE_ID}}
|
|
3
|
+
cabbage_stage: {{STAGE_ID}}
|
|
4
|
+
change_type: {{CHANGE_TYPE}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Replace every marked prompt before verifying this stage. Use N/A with a reason when a section does not apply. -->
|
|
8
|
+
|
|
9
|
+
# Context
|
|
10
|
+
|
|
11
|
+
## Current State
|
|
12
|
+
|
|
13
|
+
<!-- CABBAGE: Describe the current implementation, constraints, and the reason for change. -->
|
|
14
|
+
|
|
15
|
+
## Goals and Non-goals
|
|
16
|
+
|
|
17
|
+
- Goal: <!-- CABBAGE: State the technical outcome. -->
|
|
18
|
+
- Non-goal: <!-- CABBAGE: State what this design intentionally excludes. -->
|
|
19
|
+
|
|
20
|
+
# Requirements
|
|
21
|
+
|
|
22
|
+
| ID | Technical requirement | Source |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| TR-1 | <!-- CABBAGE: Record a functional or non-functional requirement. --> | <!-- CABBAGE: Link it to a requirement, constraint, or risk. --> |
|
|
25
|
+
|
|
26
|
+
# Design
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
<!-- CABBAGE: Explain the proposed design and the responsibilities of each affected component. -->
|
|
31
|
+
|
|
32
|
+
<!-- CABBAGE: Add a Mermaid component, sequence, or state diagram when it improves reviewability; otherwise state N/A. -->
|
|
33
|
+
|
|
34
|
+
## Interfaces and Data
|
|
35
|
+
|
|
36
|
+
<!-- CABBAGE: Describe interface changes, data flow, state transitions, invariants, and compatibility constraints. -->
|
|
37
|
+
|
|
38
|
+
## Decision Boundaries
|
|
39
|
+
|
|
40
|
+
### AI Autonomous Decisions
|
|
41
|
+
|
|
42
|
+
- <!-- CABBAGE: List internal implementation decisions AI can independently make (e.g. private helpers, internal data structures, local refactoring). -->
|
|
43
|
+
|
|
44
|
+
### Human Gate Decisions
|
|
45
|
+
|
|
46
|
+
- <!-- CABBAGE: List critical decisions requiring explicit human review/approval (e.g. breaking API changes, storage migration, security policies). -->
|
|
47
|
+
|
|
48
|
+
# Alternatives
|
|
49
|
+
|
|
50
|
+
## Architecture Options Comparison
|
|
51
|
+
|
|
52
|
+
| Option | Architecture Approach | Benefits | Costs & Risks | Recommendation |
|
|
53
|
+
|---|---|---|---|---|
|
|
54
|
+
| Option A (Recommended) | <!-- CABBAGE: Describe primary approach. --> | <!-- CABBAGE: List advantages and trade-offs. --> | <!-- CABBAGE: List costs and risks. --> | Chosen |
|
|
55
|
+
| Option B (Alternative) | <!-- CABBAGE: Describe alternative approach (e.g. minimal vs decoupled). --> | <!-- CABBAGE: List advantages. --> | <!-- CABBAGE: List trade-offs/drawbacks. --> | Rejected |
|
|
56
|
+
|
|
57
|
+
## Selected Decision & Trade-off Rationale
|
|
58
|
+
|
|
59
|
+
<!-- CABBAGE: Explain why the chosen option won over alternatives and state accepted trade-offs. -->
|
|
60
|
+
|
|
61
|
+
# Security and Privacy
|
|
62
|
+
|
|
63
|
+
<!-- CABBAGE: Describe trust boundaries, authorization, sensitive data, validation, and privacy impact, or state N/A. -->
|
|
64
|
+
|
|
65
|
+
# Observability
|
|
66
|
+
|
|
67
|
+
| Signal | Purpose | Alert or dashboard |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| <!-- CABBAGE: Name a log, metric, trace, or audit event. --> | <!-- CABBAGE: Explain what it proves. --> | <!-- CABBAGE: Describe how operators use it. --> |
|
|
70
|
+
|
|
71
|
+
# Failure Modes
|
|
72
|
+
|
|
73
|
+
| Failure mode | Detection | Handling | Recovery |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| <!-- CABBAGE: Describe a credible failure. --> | <!-- CABBAGE: Describe the signal. --> | <!-- CABBAGE: Describe runtime behavior. --> | <!-- CABBAGE: Describe recovery steps. --> |
|
|
76
|
+
|
|
77
|
+
# Rollout
|
|
78
|
+
|
|
79
|
+
<!-- CABBAGE: Describe sequencing, feature flags, compatibility windows, migration, and success criteria. -->
|
|
80
|
+
|
|
81
|
+
# Rollback
|
|
82
|
+
|
|
83
|
+
<!-- CABBAGE: Define rollback triggers, steps, and data-safety constraints. -->
|
|
84
|
+
|
|
85
|
+
# Open Questions
|
|
86
|
+
|
|
87
|
+
- <!-- CABBAGE: Record unresolved technical decisions and an owner, or state N/A. -->
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
change: {{CHANGE_ID}}
|
|
3
|
+
cabbage_stage: {{STAGE_ID}}
|
|
4
|
+
change_type: {{CHANGE_TYPE}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- Replace every marked prompt before verifying this stage. Use N/A with a reason when a section does not apply. -->
|
|
8
|
+
|
|
9
|
+
# Strategy
|
|
10
|
+
|
|
11
|
+
<!-- CABBAGE: Define the scope, test levels, public test seams, and key quality risks. -->
|
|
12
|
+
|
|
13
|
+
| Level | Scope | Test seam | Owner |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| <!-- CABBAGE: Unit, integration, contract, E2E, or manual. --> | <!-- CABBAGE: State what is covered. --> | <!-- CABBAGE: Name the public interface or observable result. --> | <!-- CABBAGE: Name the owner. --> |
|
|
16
|
+
|
|
17
|
+
# Test Environment and Data
|
|
18
|
+
|
|
19
|
+
<!-- CABBAGE: Describe required environments, fixtures, accounts, data setup, isolation, and cleanup. -->
|
|
20
|
+
|
|
21
|
+
# Cases
|
|
22
|
+
|
|
23
|
+
| ID | Scenario | Level | Expected result | Priority |
|
|
24
|
+
|---|---|---|---|---|
|
|
25
|
+
| T-1 | <!-- CABBAGE: Describe a critical happy path or failure case. --> | <!-- CABBAGE: Name the test level. --> | <!-- CABBAGE: State the observable outcome. --> | High |
|
|
26
|
+
|
|
27
|
+
# Regression Coverage
|
|
28
|
+
|
|
29
|
+
- <!-- CABBAGE: Identify existing behavior and tests that protect it from regression. -->
|
|
30
|
+
|
|
31
|
+
# Non-functional Testing
|
|
32
|
+
|
|
33
|
+
| Quality attribute | Method | Threshold |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| <!-- CABBAGE: Performance, security, resilience, accessibility, or another attribute. --> | <!-- CABBAGE: Describe the verification method. --> | <!-- CABBAGE: Define the pass threshold. --> |
|
|
36
|
+
|
|
37
|
+
# Entry and Exit Criteria
|
|
38
|
+
|
|
39
|
+
- Entry: <!-- CABBAGE: Define prerequisites for starting verification. -->
|
|
40
|
+
- Exit: <!-- CABBAGE: Define evidence required to finish verification. -->
|
|
41
|
+
|
|
42
|
+
# Risks
|
|
43
|
+
|
|
44
|
+
- <!-- CABBAGE: Record coverage gaps, flaky dependencies, or untestable assumptions and their mitigations. -->
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# One change record; specialist documents open only for declared risks.
|
|
2
|
+
record_covers: [product, testing]
|
|
3
|
+
stages:
|
|
4
|
+
- {id: design, artifact: tech-spec.md, template: tech-spec.md, when: impact.design, required_headings: [Context, Design, Failure Modes, Rollout]}
|
|
5
|
+
- {id: rfc, artifact: rfc.md, template: rfc.md, when: impact.rfc, required_headings: [Problem, Proposal, Alternatives]}
|
|
6
|
+
- {id: adr, artifact: adr.md, template: adr.md, when: impact.architecture, required_headings: [Context, Decision, Consequences]}
|
|
7
|
+
- {id: api, artifact: api-design.md, template: api-design.md, when: impact.api, required_headings: [Contract, Compatibility]}
|
|
8
|
+
- {id: database, artifact: database-design.md, template: database-design.md, when: impact.database, required_headings: [Schema, Migration, Rollback]}
|
|
9
|
+
- {id: security, artifact: security-review.md, template: security-review.md, when: impact.security, required_headings: [Threats, Controls]}
|
|
10
|
+
- {id: release, artifact: release-plan.md, template: release-plan.md, when: impact.deployment, required_headings: [Deployment, Rollback, Verification]}
|
|
11
|
+
- {id: tasks, artifact: tasks.md, template: change-record.md, depends_on: [design, rfc, adr, api, database, security, release], validator: checklist, required_headings: [Goal, Design, Tasks, Verification]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- {id: incident, artifact: incident.md, template: incident.md, required_headings: [Impact, Timeline, Mitigation]}
|
|
3
|
+
- {id: postmortem, artifact: postmortem.md, template: postmortem.md, depends_on: [incident], required_headings: [Root Cause, Contributing Factors, Corrective Actions]}
|
|
4
|
+
- {id: tasks, artifact: tasks.md, template: tasks.md, depends_on: [postmortem], validator: checklist, required_headings: [Tasks]}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import argparse, json, shutil, subprocess, sys
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from . import __version__
|
|
6
|
+
from .core import (CabbageError, archive_root, change_dir, change_spec, changes_root,
|
|
7
|
+
ci_check, condition_enabled, dump_yaml, gate, get_change_tasks_dag,
|
|
8
|
+
load_config, load_yaml, project_root, ready_to_archive, stage_map, stage_statuses,
|
|
9
|
+
sync_change_to_docs, validate_change, verify_stage, workflow)
|
|
10
|
+
from .scaffold import init_project, new_change, ensure_artifacts, adopt_project, discard_change
|
|
11
|
+
|
|
12
|
+
def emit(data):
|
|
13
|
+
# Every caller only wants JSON here; human-readable text is printed explicitly.
|
|
14
|
+
print(json.dumps(data,ensure_ascii=False,indent=2))
|
|
15
|
+
|
|
16
|
+
def cmd_init(a):
|
|
17
|
+
root=Path.cwd().resolve(); init_project(root,a.force,not a.no_vendor_cli)
|
|
18
|
+
print("initialized cabbage")
|
|
19
|
+
print("next: cabbage new change <change-id>")
|
|
20
|
+
print("existing docs to onboard? run: cabbage adopt")
|
|
21
|
+
|
|
22
|
+
def cmd_adopt(a):
|
|
23
|
+
root=project_root(); data=adopt_project(root, apply=a.apply)
|
|
24
|
+
if a.json: emit(data); return 0
|
|
25
|
+
if data.get("applied"):
|
|
26
|
+
print(f"migrated {len(data['applied'])} document(s):")
|
|
27
|
+
for item in data["applied"]:
|
|
28
|
+
print(f" {item['from']} -> {item['to']}")
|
|
29
|
+
for w in data.get("warnings", []):
|
|
30
|
+
print(f"warning: {w}")
|
|
31
|
+
counts=data["counts"]
|
|
32
|
+
print(f"adoption report written to {data['report']}")
|
|
33
|
+
print(f"documents: {sum(counts.values())} total; keep={counts['keep']} migrate={counts['migrate']} import={counts['import']} review={counts['review']}")
|
|
34
|
+
for r in data["documents"]:
|
|
35
|
+
if r["action"] in {"migrate","import","review"}:
|
|
36
|
+
target=r["target"] or "-"
|
|
37
|
+
print(f" {r['action']:8} {r['path']} -> {target}")
|
|
38
|
+
print("next: resolve review rows, then follow the cabbage-adopt skill")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
def cmd_discard(a):
|
|
42
|
+
root=project_root(); discard_change(root, a.change)
|
|
43
|
+
print(f"discarded change `{a.change}`")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
def cmd_doctor(a):
|
|
47
|
+
results = {}
|
|
48
|
+
# 1. Python
|
|
49
|
+
py_ok = sys.version_info >= (3, 10)
|
|
50
|
+
results["python"] = {
|
|
51
|
+
"ok": py_ok,
|
|
52
|
+
"version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
53
|
+
"message": "Python >= 3.10 required" if not py_ok else "OK",
|
|
54
|
+
}
|
|
55
|
+
# 2. PyYAML
|
|
56
|
+
try:
|
|
57
|
+
import yaml
|
|
58
|
+
results["pyyaml"] = {"ok": True, "version": getattr(yaml, "__version__", "unknown")}
|
|
59
|
+
except ImportError:
|
|
60
|
+
results["pyyaml"] = {"ok": False, "message": "PyYAML is not installed"}
|
|
61
|
+
|
|
62
|
+
# 3. Git
|
|
63
|
+
git_which = shutil.which("git")
|
|
64
|
+
if git_which:
|
|
65
|
+
try:
|
|
66
|
+
p = subprocess.run(["git", "--version"], capture_output=True, text=True)
|
|
67
|
+
results["git"] = {"ok": True, "version": p.stdout.strip()}
|
|
68
|
+
except Exception as e:
|
|
69
|
+
results["git"] = {"ok": False, "message": str(e)}
|
|
70
|
+
else:
|
|
71
|
+
results["git"] = {"ok": False, "message": "git executable not found in PATH"}
|
|
72
|
+
|
|
73
|
+
# 4. pnpm
|
|
74
|
+
pnpm_which = shutil.which("pnpm")
|
|
75
|
+
if pnpm_which:
|
|
76
|
+
try:
|
|
77
|
+
p = subprocess.run(["pnpm", "--version"], capture_output=True, text=True)
|
|
78
|
+
results["pnpm"] = {"ok": True, "version": p.stdout.strip()}
|
|
79
|
+
except Exception as e:
|
|
80
|
+
results["pnpm"] = {"ok": False, "message": str(e)}
|
|
81
|
+
else:
|
|
82
|
+
results["pnpm"] = {"ok": True, "version": "not installed (optional for docs dev/build)"}
|
|
83
|
+
|
|
84
|
+
# 5. Cabbage Project
|
|
85
|
+
try:
|
|
86
|
+
r = project_root()
|
|
87
|
+
cfg = load_config(r)
|
|
88
|
+
results["project"] = {"ok": True, "path": str(r), "version": cfg.get("version", 1)}
|
|
89
|
+
except Exception as e:
|
|
90
|
+
results["project"] = {"ok": False, "message": str(e)}
|
|
91
|
+
|
|
92
|
+
all_ok = results["python"]["ok"] and results["pyyaml"]["ok"] and results["git"]["ok"]
|
|
93
|
+
if a.json:
|
|
94
|
+
emit({"ok": all_ok, "checks": results})
|
|
95
|
+
else:
|
|
96
|
+
print("Cabbage Doctor Diagnosis:")
|
|
97
|
+
for name, info in results.items():
|
|
98
|
+
status = "✓" if info["ok"] else "✗"
|
|
99
|
+
ver = f"({info.get('version', '')})" if info.get('version') else ""
|
|
100
|
+
msg = f"- {info.get('message')}" if not info["ok"] or 'message' in info else ""
|
|
101
|
+
print(f" {status} {name:10} {ver} {msg}".rstrip())
|
|
102
|
+
if all_ok:
|
|
103
|
+
print("\nAll essential dependencies and environment checks passed.")
|
|
104
|
+
else:
|
|
105
|
+
print("\nSome environment checks failed.")
|
|
106
|
+
return 0 if all_ok else 1
|
|
107
|
+
|
|
108
|
+
def cmd_new(a):
|
|
109
|
+
root=project_root(); new_change(root,a.type,a.change); print(f"created {a.change}"); cmd_status(argparse.Namespace(change=a.change,json=False))
|
|
110
|
+
|
|
111
|
+
def cmd_status(a):
|
|
112
|
+
root=project_root()
|
|
113
|
+
if a.change:
|
|
114
|
+
items=stage_statuses(root,a.change)
|
|
115
|
+
if a.json: emit({"change":a.change,"stages":items})
|
|
116
|
+
else:
|
|
117
|
+
for x in items: print(f"{x['status']:7} {x['id']:16} {x.get('artifact') or ''}")
|
|
118
|
+
else:
|
|
119
|
+
dirs=changes_root(root); rows=[]
|
|
120
|
+
for p in sorted(dirs.iterdir() if dirs.exists() else []):
|
|
121
|
+
if p.is_dir() and (p/"change.yaml").exists():
|
|
122
|
+
st=stage_statuses(root,p.name)
|
|
123
|
+
done=sum(x['status']=='done' for x in st)
|
|
124
|
+
total=sum(x['status']!='skipped' for x in st)
|
|
125
|
+
stale=sum(x['status']=='stale' for x in st)
|
|
126
|
+
rows.append({"change":p.name,"done":done,"total":total,"stale":stale,
|
|
127
|
+
"ready_to_archive":ready_to_archive(st)})
|
|
128
|
+
if a.json: emit(rows)
|
|
129
|
+
elif not rows: print("no active changes")
|
|
130
|
+
else:
|
|
131
|
+
for r in rows: print(f"{r['change']:<20} {r['done']}/{r['total']} done {r['stale']} stale")
|
|
132
|
+
ready=[r["change"] for r in rows if r["ready_to_archive"]]
|
|
133
|
+
if ready:
|
|
134
|
+
print(f"\n{len(ready)} change(s) fully verified and ready to archive.")
|
|
135
|
+
print("After confirming the merge, run: cabbage archive <change-id>")
|
|
136
|
+
|
|
137
|
+
def cmd_next(a):
|
|
138
|
+
root=project_root(); sts=stage_statuses(root,a.change); status={x['id']:x['status'] for x in sts}; spec=change_spec(root,a.change); wf,_=workflow(root,spec['type']); sm=stage_map(wf)
|
|
139
|
+
ready=[]
|
|
140
|
+
for x in sts:
|
|
141
|
+
if x['status'] not in {'pending','stale'}: continue
|
|
142
|
+
deps=[d for d in sm[x['id']].get('depends_on',[]) if d in sm and condition_enabled(sm[d],spec)]
|
|
143
|
+
if all(status.get(d)=='done' for d in deps): ready.append(x)
|
|
144
|
+
data={"change":a.change,"ready":ready,"blocked":[x for x in sts if x['status'] in {'pending','stale'} and x not in ready]}
|
|
145
|
+
if a.json: emit(data)
|
|
146
|
+
else:
|
|
147
|
+
if data["ready"]:
|
|
148
|
+
print("ready:"); [print(f" {x['id']:<16} {x.get('artifact') or ''}") for x in data["ready"]]
|
|
149
|
+
else: print("no ready stages")
|
|
150
|
+
if data["blocked"]:
|
|
151
|
+
print("blocked:"); [print(f" {x['id']:<16} {x.get('artifact') or ''}") for x in data["blocked"]]
|
|
152
|
+
return 0 if ready or all(x['status'] in {'done','skipped'} for x in sts) else 2
|
|
153
|
+
|
|
154
|
+
def cmd_impact(a):
|
|
155
|
+
root=project_root(); p=change_dir(root,a.change)/"change.yaml"; spec=load_yaml(p); cfg=load_config(root)
|
|
156
|
+
for pair in a.set or []:
|
|
157
|
+
if '=' not in pair: raise CabbageError("--set expects field=true|false")
|
|
158
|
+
k,v=pair.split('=',1)
|
|
159
|
+
if k not in cfg.get('impact_fields',[]): raise CabbageError(f"unknown impact field: {k}")
|
|
160
|
+
if v.lower() not in {'true','false'}: raise CabbageError("impact value must be true or false")
|
|
161
|
+
spec.setdefault('impact',{})[k]=v.lower()=='true'
|
|
162
|
+
if a.set:
|
|
163
|
+
dump_yaml(p,spec); ensure_artifacts(root,a.change)
|
|
164
|
+
if a.json: emit({"change":a.change,"impact":spec.get('impact',{})})
|
|
165
|
+
else:
|
|
166
|
+
for k,v in spec.get('impact',{}).items(): print(f" {k:<12} {'Yes' if v else 'No'}")
|
|
167
|
+
|
|
168
|
+
def cmd_validate(a):
|
|
169
|
+
root=project_root(); errors=[]
|
|
170
|
+
ids=[]
|
|
171
|
+
if a.all:
|
|
172
|
+
ids=[p.name for p in changes_root(root).iterdir() if p.is_dir() and (p/'change.yaml').exists()]
|
|
173
|
+
elif a.change: ids=[a.change]
|
|
174
|
+
else: raise CabbageError("provide a change id or --all")
|
|
175
|
+
for cid in ids: errors.extend(f"{cid}: {e}" for e in validate_change(root,cid))
|
|
176
|
+
if a.json: emit({"ok":not errors,"errors":errors})
|
|
177
|
+
elif errors:
|
|
178
|
+
print("INVALID"); [print(f"- {e}") for e in errors]
|
|
179
|
+
else: print("VALID")
|
|
180
|
+
return 1 if errors else 0
|
|
181
|
+
|
|
182
|
+
def cmd_verify(a):
|
|
183
|
+
root=project_root(); verify_stage(root,a.change,a.stage); print(f"verified {a.change}:{a.stage}")
|
|
184
|
+
|
|
185
|
+
def cmd_sync(a):
|
|
186
|
+
root=project_root(); synced=sync_change_to_docs(root,a.change)
|
|
187
|
+
if a.json: emit({"change":a.change,"synced":synced})
|
|
188
|
+
else:
|
|
189
|
+
if synced:
|
|
190
|
+
print(f"synced {len(synced)} document(s) to current-state docs:")
|
|
191
|
+
for path in synced: print(f" {path}")
|
|
192
|
+
else:
|
|
193
|
+
print("no documents to sync")
|
|
194
|
+
|
|
195
|
+
def cmd_gate(a):
|
|
196
|
+
root=project_root(); errors=gate(root,a.change,a.target)
|
|
197
|
+
if a.json: emit({"ok":not errors,"errors":errors})
|
|
198
|
+
elif errors:
|
|
199
|
+
print("BLOCKED"); [print(f"- {e}") for e in errors]
|
|
200
|
+
else: print("ALLOWED")
|
|
201
|
+
return 1 if errors else 0
|
|
202
|
+
|
|
203
|
+
def cmd_archive(a):
|
|
204
|
+
root=project_root(); errors=gate(root,a.change,'archive')
|
|
205
|
+
if errors: raise CabbageError("archive gate blocked:\n"+'\n'.join(errors))
|
|
206
|
+
synced=sync_change_to_docs(root,a.change)
|
|
207
|
+
d=change_dir(root,a.change); spec=load_yaml(d/'change.yaml'); spec['status']='archived'; dump_yaml(d/'change.yaml',spec)
|
|
208
|
+
dest=archive_root(root)/datetime.now().strftime('%Y')/a.change; dest.parent.mkdir(parents=True,exist_ok=True)
|
|
209
|
+
if dest.exists(): raise CabbageError(f"archive destination exists: {dest}")
|
|
210
|
+
shutil.move(str(d),str(dest))
|
|
211
|
+
if synced:
|
|
212
|
+
print(f"synced {len(synced)} document(s) to current-state docs")
|
|
213
|
+
print(f"archived to {dest.relative_to(root)}")
|
|
214
|
+
|
|
215
|
+
def cmd_ci(a):
|
|
216
|
+
root=project_root(); errors=ci_check(root,a.base)
|
|
217
|
+
if errors:
|
|
218
|
+
print("CABBAGE CI FAILED"); [print(f"- {e}") for e in errors]; return 1
|
|
219
|
+
print("CABBAGE CI PASSED"); return 0
|
|
220
|
+
|
|
221
|
+
def cmd_docs(a):
|
|
222
|
+
root=project_root(); docs=root/load_config(root).get('docs',{}).get('dir','docs')
|
|
223
|
+
if a.action=='install': cmd=['pnpm','install']
|
|
224
|
+
elif a.action=='dev': cmd=['pnpm','run','dev']
|
|
225
|
+
else: cmd=['pnpm','run','build']
|
|
226
|
+
if shutil.which('pnpm') is None: raise CabbageError('pnpm not found')
|
|
227
|
+
return subprocess.call(cmd,cwd=docs)
|
|
228
|
+
|
|
229
|
+
def cmd_tasks(a):
|
|
230
|
+
root=project_root(); data=get_change_tasks_dag(root, a.change)
|
|
231
|
+
if a.json: emit(data); return 0
|
|
232
|
+
|
|
233
|
+
print(f"Task DAG for `{data['change']}`:")
|
|
234
|
+
print(f" Summary: {data['total_tasks']} total | {data['completed_tasks']} completed | {data['ready_tasks']} ready | {data['blocked_tasks']} blocked\n")
|
|
235
|
+
for grp, grp_tasks in data["parallel_groups"].items():
|
|
236
|
+
print(f" [{grp}]")
|
|
237
|
+
for t in grp_tasks:
|
|
238
|
+
status_tag = "[DONE] " if t["is_completed"] else ("[READY] " if t["is_ready"] else "[BLOCKED]")
|
|
239
|
+
blocked_info = f" (Blocked by: {', '.join(t['blocked_by'])})" if t["blocked_by"] and not t["is_completed"] else ""
|
|
240
|
+
print(f" {status_tag} {t['task_id']}: {t['title']}{blocked_info}")
|
|
241
|
+
if t["verification"] and t["verification"] != "N/A":
|
|
242
|
+
print(f" Verification: {t['verification']}")
|
|
243
|
+
print()
|
|
244
|
+
|
|
245
|
+
if not data.get("structured"):
|
|
246
|
+
print("Plain checklist: no declared dependencies or verification commands.")
|
|
247
|
+
print("Verification results belong in the record's Verification section;")
|
|
248
|
+
print("use `## Task <id>: <title>` sections only when parallelism or blocking matters.")
|
|
249
|
+
elif data["completed_tasks"] == data["total_tasks"] and data["total_tasks"] > 0:
|
|
250
|
+
print("All tasks in DAG completed. Ready for verification & gate.")
|
|
251
|
+
elif data["ready_tasks"] == 0:
|
|
252
|
+
print("No tasks currently ready (check prerequisites or dependencies).")
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
def parser():
|
|
256
|
+
p=argparse.ArgumentParser(prog='cabbage'); p.add_argument('--version',action='version',version=__version__); sp=p.add_subparsers(dest='cmd',required=True)
|
|
257
|
+
x=sp.add_parser('init'); x.add_argument('--force',action='store_true'); x.add_argument('--no-vendor-cli',action='store_true'); x.set_defaults(func=cmd_init)
|
|
258
|
+
x=sp.add_parser('adopt'); x.add_argument('--apply',action='store_true'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_adopt)
|
|
259
|
+
x=sp.add_parser('doctor'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_doctor)
|
|
260
|
+
x=sp.add_parser('new'); x.add_argument('type'); x.add_argument('change'); x.set_defaults(func=cmd_new)
|
|
261
|
+
x=sp.add_parser('discard'); x.add_argument('change'); x.set_defaults(func=cmd_discard)
|
|
262
|
+
x=sp.add_parser('status'); x.add_argument('change',nargs='?'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_status)
|
|
263
|
+
x=sp.add_parser('next'); x.add_argument('change'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_next)
|
|
264
|
+
x=sp.add_parser('impact'); x.add_argument('change'); x.add_argument('--set',action='append'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_impact)
|
|
265
|
+
x=sp.add_parser('validate'); x.add_argument('change',nargs='?'); x.add_argument('--all',action='store_true'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_validate)
|
|
266
|
+
x=sp.add_parser('verify'); x.add_argument('change'); x.add_argument('stage'); x.set_defaults(func=cmd_verify)
|
|
267
|
+
x=sp.add_parser('sync'); x.add_argument('change'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_sync)
|
|
268
|
+
x=sp.add_parser('gate'); x.add_argument('change'); x.add_argument('target'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_gate)
|
|
269
|
+
x=sp.add_parser('archive'); x.add_argument('change'); x.set_defaults(func=cmd_archive)
|
|
270
|
+
x=sp.add_parser('ci'); x.add_argument('--base',required=True); x.set_defaults(func=cmd_ci)
|
|
271
|
+
x=sp.add_parser('tasks'); x.add_argument('change'); x.add_argument('--json',action='store_true'); x.set_defaults(func=cmd_tasks)
|
|
272
|
+
x=sp.add_parser('docs'); x.add_argument('action',choices=['install','dev','build']); x.set_defaults(func=cmd_docs)
|
|
273
|
+
return p
|
|
274
|
+
|
|
275
|
+
def main(argv=None):
|
|
276
|
+
try:
|
|
277
|
+
a=parser().parse_args(argv); return a.func(a) or 0
|
|
278
|
+
except CabbageError as e:
|
|
279
|
+
print(f"cabbage: {e}",file=sys.stderr); return 2
|
|
280
|
+
except KeyboardInterrupt:
|
|
281
|
+
print("\naborted by user",file=sys.stderr); return 130
|
|
282
|
+
|
|
283
|
+
if __name__=='__main__': raise SystemExit(main())
|