aw-spec-cli 0.1.4 → 0.1.5
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/assets/.agents/agent.json +28 -0
- package/assets/.agents/skills/aw-spec-orch/SKILL.md +56 -0
- package/assets/.agents/skills/aw-spec-orch-demo/SKILL.md +59 -0
- package/assets/.aw-spec/DESIGN-FORMAT.md +151 -0
- package/assets/.aw-spec/ORCH-DIALECT.md +156 -0
- package/assets/.aw-spec/RULE-PARTITION.md +126 -0
- package/assets/.aw-spec/extensions/agent-context/extension.yml +12 -9
- package/assets/.aw-spec/extensions.yml +12 -19
- package/assets/.aw-spec/integrations/aw-spec.manifest.json +1 -9
- package/assets/.aw-spec/interpreter/run-skill.py +1158 -0
- package/assets/.aw-spec/orchestrator/AGENT-PROFILE.md +94 -0
- package/assets/.aw-spec/orchestrator/README.md +300 -0
- package/assets/.aw-spec/orchestrator/adapters/claude_agent_sdk.py +74 -0
- package/assets/.aw-spec/orchestrator/adapters/google_antigravity.py +74 -0
- package/assets/.aw-spec/orchestrator/modules/delta-design.orch.yaml +105 -0
- package/assets/.aw-spec/orchestrator/modules/delta-spec.orch.yaml +79 -0
- package/assets/.aw-spec/orchestrator/modules/domain-select.orch.yaml +81 -0
- package/assets/.aw-spec/orchestrator/modules/tech-select.orch.yaml +75 -0
- package/assets/.aw-spec/orchestrator/orchestrator.py +1721 -0
- package/assets/.aw-spec/orchestrator/rev-templates/ai-rules.reference.md +40 -0
- package/assets/.aw-spec/orchestrator/rev-templates/comandrulemap.skeleton.md +59 -0
- package/assets/.aw-spec/orchestrator/rev-templates/commandtechrulemap.skeleton.md +97 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-constitution.skeleton.md +61 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-requirements.skeleton.md +48 -0
- package/assets/.aw-spec/orchestrator/rev-templates/domain-rule-file.skeleton.md +39 -0
- package/assets/.aw-spec/orchestrator/rev-templates/technical-constitution.skeleton.md +106 -0
- package/assets/.aw-spec/orchestrator/rev-templates/technical-rule-file.skeleton.md +54 -0
- package/assets/.aw-spec/orchestrator/workflows/_smoke.orch.yaml +160 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-add-component.orch.yaml +244 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-adopt.orch.yaml +775 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-delta-design.orch.yaml +248 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-design.orch.yaml +261 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-analyze-slice.orch.yaml +425 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-build.orch.yaml +188 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-characterize.orch.yaml +474 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-close-delta.orch.yaml +181 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-constitution.orch.yaml +284 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-demo.orch.yaml +103 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-dependency-tree.orch.yaml +69 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-deploy.orch.yaml +85 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-design.orch.yaml +382 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-finalize-delta.orch.yaml +382 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-foundation.orch.yaml +246 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement-delta.orch.yaml +231 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-implement.orch.yaml +258 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-inventory.orch.yaml +236 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-onboard.orch.yaml +949 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-parity.orch.yaml +293 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-run.orch.yaml +177 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-domain.orch.yaml +79 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-select-feature.orch.yaml +128 -0
- package/assets/.aw-spec/orchestrator/workflows/aw-spec-specify.orch.yaml +412 -0
- package/assets/.aw-spec/scripts/bash/aw-inventory.sh +14 -0
- package/assets/.aw-spec/scripts/bash/aw-parity.sh +13 -0
- package/assets/.aw-spec/scripts/bash/aw-rev-constitution.sh +13 -0
- package/assets/.aw-spec/scripts/bash/aw-spec-actions.sh +1899 -0
- package/assets/.aw-spec/scripts/bash/aw-xref.sh +15 -0
- package/assets/.aw-spec/scripts/bash/common.sh +11 -60
- package/assets/.aw-spec/scripts/powershell/aw-inventory.ps1 +14 -0
- package/assets/.aw-spec/scripts/powershell/aw-parity.ps1 +13 -0
- package/assets/.aw-spec/scripts/powershell/aw-rev-constitution.ps1 +13 -0
- package/assets/.aw-spec/scripts/powershell/aw-spec-actions.ps1 +1563 -0
- package/assets/.aw-spec/scripts/powershell/aw-xref.ps1 +15 -0
- package/assets/.aw-spec/scripts/python/aw-inventory.py +683 -0
- package/assets/.aw-spec/scripts/python/aw-parity.py +380 -0
- package/assets/.aw-spec/scripts/python/aw-rev-constitution.py +312 -0
- package/assets/.aw-spec/scripts/python/aw-xref.py +1330 -0
- package/assets/.aw-spec/scripts/python/validate_okf.py +349 -0
- package/assets/.aw-spec/templates/constitution-domain-template.md +75 -0
- package/assets/.aw-spec/templates/constitution-technical-template.md +70 -0
- package/assets/.aw-spec/templates/delta-design-template.md +69 -0
- package/assets/.aw-spec/templates/delta-spec-template.md +60 -0
- package/assets/.aw-spec/templates/design-template.md +383 -0
- package/assets/.aw-spec/templates/okf-concept-template.md +103 -0
- package/assets/.aw-spec/templates/parity-manifest-template.json +49 -0
- package/assets/.aw-spec/templates/source-map-template.md +85 -0
- package/assets/.aw-spec/templates/spec-template.md +192 -71
- package/assets/.claude/agent.json +27 -0
- package/assets/.claude/skills/aw-spec-orch/SKILL.md +55 -0
- package/assets/.claude/skills/aw-spec-orch-demo/SKILL.md +59 -0
- package/assets/.kilocode/agent.json +28 -0
- package/assets/.kilocode/workflows/aw-spec-orch-demo.md +59 -0
- package/assets/.kilocode/workflows/aw-spec-orch.md +55 -0
- package/assets/docs/architecture/clean-architecture.md +1 -1
- package/assets/docs/architecture/cqrs.md +1 -1
- package/assets/docs/architecture/eventing.md +1 -1
- package/assets/docs/architecture/folder-structure.md +1 -1
- package/assets/docs/architecture/modular-monolith.md +1 -1
- package/assets/docs/architecture/persistence.md +1 -1
- package/assets/docs/architecture/testing.md +1 -1
- package/assets/docs/architecture/vertical-slices.md +1 -1
- package/assets/docs/components/Email/README.md +1 -1
- package/assets/docs/components/Jobs/README.md +1 -1
- package/assets/docs/components/QrGenerator/README.md +1 -1
- package/assets/docs/components/README.md +1 -1
- package/assets/docs/domain/domain-driven-design.md +1 -1
- package/assets/docs/domainspecs/healthcare/api-versioning.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/auditing.domain.md +86 -18
- package/assets/docs/domainspecs/healthcare/authorization.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/clinical-terms.md +1 -1
- package/assets/docs/domainspecs/healthcare/coding-standards.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/comandrulemap.md +58 -124
- package/assets/docs/domainspecs/healthcare/commandtechrulemap.md +119 -0
- package/assets/docs/domainspecs/healthcare/consent.md +1 -1
- package/assets/docs/domainspecs/healthcare/cqrs.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/data-classification.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/dependency-policy.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/dicom.md +1 -1
- package/assets/docs/domainspecs/healthcare/domain-driven-design.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/eventing.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/fhir.md +1 -1
- package/assets/docs/domainspecs/healthcare/folder-structure.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/hipaa.md +1 -1
- package/assets/docs/domainspecs/healthcare/hl7.md +1 -1
- package/assets/docs/domainspecs/healthcare/integration-terms.md +1 -1
- package/assets/docs/domainspecs/healthcare/persistence.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/terminology.md +1 -1
- package/assets/docs/domainspecs/healthcare/testing.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/vertical-slices.domain.md +1 -1
- package/assets/docs/domainspecs/healthcare/zero-trust.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/api-versioning.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/atc.md +1 -1
- package/assets/docs/domainspecs/pharma/auditing.domain.md +80 -16
- package/assets/docs/domainspecs/pharma/authorization.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/coding-standards.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/comandrulemap.md +64 -142
- package/assets/docs/domainspecs/pharma/commandtechrulemap.md +119 -0
- package/assets/docs/domainspecs/pharma/cqrs.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/data-classification.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/data-provenance.md +1 -1
- package/assets/docs/domainspecs/pharma/dependency-policy.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/domain-driven-design.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/eventing.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/fhir.md +1 -1
- package/assets/docs/domainspecs/pharma/folder-structure.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/hl7.md +1 -1
- package/assets/docs/domainspecs/pharma/idmp.md +1 -1
- package/assets/docs/domainspecs/pharma/integration-terms.md +1 -1
- package/assets/docs/domainspecs/pharma/ndc.md +1 -1
- package/assets/docs/domainspecs/pharma/persistence.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/pharma-terms.md +1 -1
- package/assets/docs/domainspecs/pharma/product-lifecycle.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/regulatory-compliance.md +1 -1
- package/assets/docs/domainspecs/pharma/terminology.md +1 -1
- package/assets/docs/domainspecs/pharma/testing.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/vertical-slices.domain.md +1 -1
- package/assets/docs/domainspecs/pharma/zero-trust.domain.md +1 -1
- package/assets/docs/governance/api-versioning.md +1 -1
- package/assets/docs/governance/coding-standards.md +1 -1
- package/assets/docs/governance/dependency-policy.md +1 -1
- package/assets/docs/requirement-spec.txt +27 -0
- package/assets/docs/security/auditing.md +54 -11
- package/assets/docs/security/authorization.md +1 -1
- package/assets/docs/security/data-classification.md +1 -1
- package/assets/docs/security/zero-trust.md +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -54
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/paths.js +2 -2
- package/dist/utils/paths.js.map +1 -1
- package/package.json +1 -1
- package/assets/.agents/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.agents/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.agents/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.agents/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.agents/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.agents/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.agents/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.agents/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.agents/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.agents/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.agents/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.agents/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.agents/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.agents/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.agents/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- package/assets/.aw-spec/integrations/claude.manifest.json +0 -17
- package/assets/.aw-spec/scripts/bash/check-prerequisites.sh +0 -189
- package/assets/.aw-spec/scripts/bash/create-new-feature.sh +0 -299
- package/assets/.aw-spec/scripts/bash/setup-plan.sh +0 -84
- package/assets/.aw-spec/scripts/bash/setup-tasks.sh +0 -91
- package/assets/.aw-spec/templates/checklist-template.md +0 -40
- package/assets/.aw-spec/templates/constitution-template.md +0 -57
- package/assets/.aw-spec/templates/plan-template.md +0 -113
- package/assets/.aw-spec/templates/tasks-template.md +0 -252
- package/assets/.aw-spec/workflows/aw-spec/workflow.yml +0 -77
- package/assets/.aw-spec/workflows/workflow-registry.json +0 -13
- package/assets/.claude/skills/aw-spec-addcomponent/SKILL.md +0 -129
- package/assets/.claude/skills/aw-spec-agent-context-update/SKILL.md +0 -31
- package/assets/.claude/skills/aw-spec-analyze/SKILL.md +0 -296
- package/assets/.claude/skills/aw-spec-checklist/SKILL.md +0 -374
- package/assets/.claude/skills/aw-spec-clarify/SKILL.md +0 -320
- package/assets/.claude/skills/aw-spec-constitution/SKILL.md +0 -194
- package/assets/.claude/skills/aw-spec-fast-implement/SKILL.md +0 -268
- package/assets/.claude/skills/aw-spec-foundation/SKILL.md +0 -358
- package/assets/.claude/skills/aw-spec-implement/SKILL.md +0 -279
- package/assets/.claude/skills/aw-spec-minimal-implementation/SKILL.md +0 -209
- package/assets/.claude/skills/aw-spec-plan/SKILL.md +0 -221
- package/assets/.claude/skills/aw-spec-select-domain/SKILL.md +0 -73
- package/assets/.claude/skills/aw-spec-specify/SKILL.md +0 -382
- package/assets/.claude/skills/aw-spec-tasks/SKILL.md +0 -259
- package/assets/.claude/skills/aw-spec-taskstoissues/SKILL.md +0 -107
- package/assets/.kilocode/rules/specify-rules.md +0 -4
- package/assets/.kilocode/workflows/aw-spec-addcomponent.md +0 -129
- package/assets/.kilocode/workflows/aw-spec-agent-context.update.md +0 -31
- package/assets/.kilocode/workflows/aw-spec-analyze.md +0 -296
- package/assets/.kilocode/workflows/aw-spec-checklist.md +0 -374
- package/assets/.kilocode/workflows/aw-spec-clarify.md +0 -320
- package/assets/.kilocode/workflows/aw-spec-constitution.md +0 -194
- package/assets/.kilocode/workflows/aw-spec-fast-implement.md +0 -263
- package/assets/.kilocode/workflows/aw-spec-foundation.md +0 -358
- package/assets/.kilocode/workflows/aw-spec-implement.md +0 -279
- package/assets/.kilocode/workflows/aw-spec-minimal-implementation.md +0 -204
- package/assets/.kilocode/workflows/aw-spec-plan.md +0 -221
- package/assets/.kilocode/workflows/aw-spec-select-domain.md +0 -73
- package/assets/.kilocode/workflows/aw-spec-specify.md +0 -382
- package/assets/.kilocode/workflows/aw-spec-tasks.md +0 -259
- package/assets/.kilocode/workflows/aw-spec-taskstoissues.md +0 -107
- package/assets/docs/UI.Visualization/PACKAGING.md +0 -191
- package/assets/docs/UI.Visualization/UI.Visualization.Reference.pack.txt +0 -11768
- package/assets/docs/UI.Visualization/UI.Visualization.pack.txt +0 -11214
- package/assets/docs/UI.Visualization/pack-foundation.ps1 +0 -165
- package/assets/docs/UI.Visualization/pack-foundation.sh +0 -206
- package/assets/docs/UI.Visualization/pack-reference-foundation.ps1 +0 -159
- package/assets/docs/UI.Visualization/pack-reference-foundation.sh +0 -205
- package/assets/docs/UI.Visualization/unpack-foundation.ps1 +0 -126
- package/assets/docs/UI.Visualization/unpack-foundation.sh +0 -217
- package/assets/docs/UI.Visualization/unpack-reference-foundation.ps1 +0 -129
- package/assets/docs/UI.Visualization/unpack-reference-foundation.sh +0 -218
- package/assets/docs/command.txt +0 -295
- package/assets/docs/domainspecs/UIVisualization/chart-bubble.md +0 -70
- package/assets/docs/domainspecs/UIVisualization/chart-dimred.md +0 -58
- package/assets/docs/domainspecs/UIVisualization/chart-heatmap.md +0 -62
- package/assets/docs/domainspecs/UIVisualization/chart-network.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-parallel.md +0 -75
- package/assets/docs/domainspecs/UIVisualization/chart-scurve.md +0 -66
- package/assets/docs/domainspecs/UIVisualization/chart-survival.md +0 -69
- package/assets/docs/domainspecs/UIVisualization/chart-trellis.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/chart-violin.md +0 -61
- package/assets/docs/domainspecs/UIVisualization/chart-volcano.md +0 -63
- package/assets/docs/domainspecs/UIVisualization/charting-standards.domain.md +0 -80
- package/assets/docs/domainspecs/UIVisualization/clean-architecture.domain.md +0 -87
- package/assets/docs/domainspecs/UIVisualization/coding-standards.domain.md +0 -47
- package/assets/docs/domainspecs/UIVisualization/comandrulemap.md +0 -130
- package/assets/docs/domainspecs/UIVisualization/component-contracts.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/data-source.domain.md +0 -68
- package/assets/docs/domainspecs/UIVisualization/domain-requirements.md +0 -110
- package/assets/docs/domainspecs/UIVisualization/folder-structure.domain.md +0 -55
- package/assets/docs/domainspecs/UIVisualization/state-management.domain.md +0 -67
- package/assets/docs/domainspecs/UIVisualization/testing.domain.md +0 -52
- package/assets/docs/domainspecs/UIVisualization/ui-viz-terms.md +0 -39
- package/assets/docs/pharma-constitution.my-ref.md +0 -86
|
@@ -0,0 +1,1899 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# aw-spec-actions.sh — common action dispatcher for declarative aw-spec skills.
|
|
4
|
+
#
|
|
5
|
+
# ONE shared script for every integration (.claude / .agents / .kilocode). The
|
|
6
|
+
# declarative skill.yaml files reference these subcommands via `execute_script`.
|
|
7
|
+
# Each subcommand performs deterministic, governance-safe work and prints a single
|
|
8
|
+
# JSON object on stdout. NONE of these actions author spec/code content — content
|
|
9
|
+
# generation is the LLM's job (the skill's single `llm_generate` step).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# aw-spec-actions.sh <subcommand> [options]
|
|
13
|
+
#
|
|
14
|
+
# Subcommands:
|
|
15
|
+
# resolve-domain --command <name> -> {domain, rulemap_exists, candidates[]}
|
|
16
|
+
# resolve-tech --command <name> -> {domain, techrulemap_exists, candidates[]}
|
|
17
|
+
# scaffold --feature-dir <path> [--allow-existing]
|
|
18
|
+
# -> {feature_dir, spec_file, template, created, spec_created}
|
|
19
|
+
# resolve-hooks --phase <before_specify|after_specify>
|
|
20
|
+
# -> {phase, hooks[]}
|
|
21
|
+
# create-checklist --feature-dir <path> [--feature-name <name>]
|
|
22
|
+
# -> {checklist_file, created}
|
|
23
|
+
# list-domains -> {domains[]}
|
|
24
|
+
# set-domain --domain <name> -> {domain, map_path, map_exists, written}
|
|
25
|
+
# list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel} (specs/ ∪ OKF aggregates)
|
|
26
|
+
# set-feature --feature <name> -> {feature, feature_dir, spec_file, existed, created, spec_exists}
|
|
27
|
+
# version-read [--feature-dir <path>] -> {feature, version, active_change, exists}
|
|
28
|
+
# version-init [--feature-dir <path>] -> {feature, version, active_change, created} (create active_version.json v1.0)
|
|
29
|
+
# feature-paths -> {feature, feature_dir, spec_file, design_file, spec_exists, design_exists}
|
|
30
|
+
# checklist-read --file <path> -> {exists, total, checked, items[]}
|
|
31
|
+
# checklist-apply --file <path> --results <json>-> {exists, before_checked, after_checked, total, newly_passing[], regressions[]}
|
|
32
|
+
# detect-project -> {scaffolded, markers[]}
|
|
33
|
+
# foundation-list [--args '<F> <A> [out]'] -> {foundations[], count, preselect_name, preselect_app, out_dir}
|
|
34
|
+
# foundation-locate --name <F> --app <A> [--out <d>]
|
|
35
|
+
# -> {ok, reason, name, app, out_dir, pack_file, unpacker, runner, sentinel, file_count, available[]}
|
|
36
|
+
# foundation-unpack --name <F> --app <A> --out <d> [--dry-run] [--force <yes|no>] [--reference]
|
|
37
|
+
# -> {ok, mode, written, has_collisions, stdout}
|
|
38
|
+
# foundation-verify --out <d> --sentinel <s> -> {clean, residue[]}
|
|
39
|
+
# list-components -> {components[]} (docs/components/* with integration.md)
|
|
40
|
+
# find-host-project -> {found, program_cs, project_dir, csproj}
|
|
41
|
+
# setup-constitution -> {constitution_technical_file, constitution_domain_file, technical_created, domain_created}
|
|
42
|
+
# compose-constitution -> {composed_file, ok, issues}
|
|
43
|
+
# agent-context-update [--plan <path>] -> {ok, ran, message}
|
|
44
|
+
#
|
|
45
|
+
# design-scaffold --feature-dir <path> [--template <name>]
|
|
46
|
+
# -> {feature_dir, design_doc, created, override_active, template, template_source}
|
|
47
|
+
# (design-extract / design-reconcile / design-paths are RETIRED — the path-tagged
|
|
48
|
+
# four-document model is gone. "Which files is this slice made of" is now an OKF
|
|
49
|
+
# query: aw-xref concept-paths / concept-upsert --merge.)
|
|
50
|
+
# build-compile [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
|
|
51
|
+
# test-run [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
|
|
52
|
+
# run-migrate [--feature-dir <path>] -> {ok, tool, ran, cmd, log_tail}
|
|
53
|
+
# deploy-dispatch -> {ok, configured, ran, cmd, log_tail}
|
|
54
|
+
# cr-setup --cr-id <id> [--feature-dir <path>] [--title <t>]
|
|
55
|
+
# -> {cr_dir, delta_spec, delta_design, created}
|
|
56
|
+
# cr-archive --cr-id <id> [--feature-dir <path>]
|
|
57
|
+
# -> {ok, archived_dir, version, previous}
|
|
58
|
+
#
|
|
59
|
+
# All subcommands accept --help.
|
|
60
|
+
|
|
61
|
+
set -e
|
|
62
|
+
|
|
63
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
64
|
+
# shellcheck source=common.sh
|
|
65
|
+
source "$SCRIPT_DIR/common.sh"
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────── small JSON helpers ────────────────────────────
|
|
68
|
+
# Emit a JSON array of strings from remaining args.
|
|
69
|
+
json_str_array() {
|
|
70
|
+
local out="[" first=1 s
|
|
71
|
+
for s in "$@"; do
|
|
72
|
+
[ $first -eq 1 ] && first=0 || out+=","
|
|
73
|
+
out+="\"$(json_escape "$s")\""
|
|
74
|
+
done
|
|
75
|
+
out+="]"
|
|
76
|
+
printf '%s' "$out"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ─────────────────────────────── resolve-domain ────────────────────────────
|
|
80
|
+
cmd_resolve_domain() {
|
|
81
|
+
local command_name="aw-spec-specify"
|
|
82
|
+
while [ $# -gt 0 ]; do
|
|
83
|
+
case "$1" in
|
|
84
|
+
--command) command_name="$2"; shift 2 ;;
|
|
85
|
+
--help|-h) echo "resolve-domain --command <name>"; return 0 ;;
|
|
86
|
+
*) echo "resolve-domain: unknown arg '$1'" >&2; return 1 ;;
|
|
87
|
+
esac
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
91
|
+
local domain_json="$repo_root/.aw-spec/domain.json"
|
|
92
|
+
local domain=""
|
|
93
|
+
if [ -f "$domain_json" ]; then
|
|
94
|
+
if command -v jq >/dev/null 2>&1; then
|
|
95
|
+
domain=$(jq -r '.domain // ""' "$domain_json" 2>/dev/null || echo "")
|
|
96
|
+
else
|
|
97
|
+
domain=$( { grep -E '"domain"[[:space:]]*:' "$domain_json" 2>/dev/null || true; } \
|
|
98
|
+
| head -n1 | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
local rulemap="$repo_root/docs/domainspecs/$domain/comandrulemap.md"
|
|
103
|
+
local rulemap_exists=false
|
|
104
|
+
[ -n "$domain" ] && [ -f "$rulemap" ] && rulemap_exists=true
|
|
105
|
+
|
|
106
|
+
# Collect bullet paths under "## /<command>" in the domain's comandrulemap.md.
|
|
107
|
+
local -a candidates=()
|
|
108
|
+
if [ "$rulemap_exists" = true ]; then
|
|
109
|
+
# awk: capture "- path" bullets between the target "## <heading>" and the next "## "
|
|
110
|
+
while IFS= read -r line; do
|
|
111
|
+
[ -n "$line" ] && candidates+=("$line")
|
|
112
|
+
done < <(awk -v h="## /$command_name" '
|
|
113
|
+
$0==h {grab=1; next}
|
|
114
|
+
/^## / {grab=0}
|
|
115
|
+
grab && /^[[:space:]]*-[[:space:]]+/ {
|
|
116
|
+
sub(/^[[:space:]]*-[[:space:]]+/,""); sub(/[[:space:]]+$/,""); print
|
|
117
|
+
}
|
|
118
|
+
' "$rulemap")
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
printf '{"domain":"%s","rulemap_exists":%s,"candidates":%s}\n' \
|
|
122
|
+
"$(json_escape "$domain")" "$rulemap_exists" "$(json_str_array "${candidates[@]}")"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ──────────────────────────────── resolve-tech ─────────────────────────────
|
|
126
|
+
# Technical rule-file mapping — parallel to resolve-domain, but reads
|
|
127
|
+
# docs/domainspecs/<domain>/commandtechrulemap.md. The TECHNICAL rule files (the
|
|
128
|
+
# docs/architecture · docs/security · docs/interop · docs/governance rule set that
|
|
129
|
+
# used to be hardcoded in each workflow's `standards:` block) are now configured
|
|
130
|
+
# per-command in this map, so they can be tuned per project WITHOUT editing the
|
|
131
|
+
# workflows. The technical CONSTITUTION (constitution.technical.md) is NOT mapped —
|
|
132
|
+
# it is still referred to directly by each consumer, as-is.
|
|
133
|
+
cmd_resolve_tech() {
|
|
134
|
+
local command_name="aw-spec-design"
|
|
135
|
+
while [ $# -gt 0 ]; do
|
|
136
|
+
case "$1" in
|
|
137
|
+
--command) command_name="$2"; shift 2 ;;
|
|
138
|
+
--help|-h) echo "resolve-tech --command <name>"; return 0 ;;
|
|
139
|
+
*) echo "resolve-tech: unknown arg '$1'" >&2; return 1 ;;
|
|
140
|
+
esac
|
|
141
|
+
done
|
|
142
|
+
|
|
143
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
144
|
+
local domain_json="$repo_root/.aw-spec/domain.json"
|
|
145
|
+
local domain=""
|
|
146
|
+
if [ -f "$domain_json" ]; then
|
|
147
|
+
if command -v jq >/dev/null 2>&1; then
|
|
148
|
+
domain=$(jq -r '.domain // ""' "$domain_json" 2>/dev/null || echo "")
|
|
149
|
+
else
|
|
150
|
+
domain=$( { grep -E '"domain"[[:space:]]*:' "$domain_json" 2>/dev/null || true; } \
|
|
151
|
+
| head -n1 | sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
|
|
152
|
+
fi
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
local techmap="$repo_root/docs/domainspecs/$domain/commandtechrulemap.md"
|
|
156
|
+
local techmap_exists=false
|
|
157
|
+
[ -n "$domain" ] && [ -f "$techmap" ] && techmap_exists=true
|
|
158
|
+
|
|
159
|
+
# Collect bullet paths under "## /<command>" (try the exact command heading,
|
|
160
|
+
# then the decl-stripped name, mirroring resolve-domain's fallback).
|
|
161
|
+
local -a candidates=()
|
|
162
|
+
if [ "$techmap_exists" = true ]; then
|
|
163
|
+
local heading
|
|
164
|
+
for heading in "/$command_name" "/${command_name/decl-/}"; do
|
|
165
|
+
while IFS= read -r line; do
|
|
166
|
+
[ -n "$line" ] && candidates+=("$line")
|
|
167
|
+
done < <(awk -v h="## $heading" '
|
|
168
|
+
$0==h {grab=1; next}
|
|
169
|
+
/^## / {grab=0}
|
|
170
|
+
grab && /^[[:space:]]*-[[:space:]]+/ {
|
|
171
|
+
sub(/^[[:space:]]*-[[:space:]]+/,""); sub(/[[:space:]]+$/,""); print
|
|
172
|
+
}
|
|
173
|
+
' "$techmap")
|
|
174
|
+
[ ${#candidates[@]} -gt 0 ] && break
|
|
175
|
+
done
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
printf '{"domain":"%s","techrulemap_exists":%s,"candidates":%s}\n' \
|
|
179
|
+
"$(json_escape "$domain")" "$techmap_exists" "$(json_str_array "${candidates[@]}")"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ─────────────────────────────── derive-names ──────────────────────────────
|
|
183
|
+
# Deterministic naming — NO LLM. Aggregate/Feature token wins; else kebab
|
|
184
|
+
# short-name (stop-word filtered) + General aggregate.
|
|
185
|
+
_pascal() { # kebab/space -> PascalCase
|
|
186
|
+
local in="$1" out="" w
|
|
187
|
+
in="${in//-/ }"
|
|
188
|
+
for w in $in; do
|
|
189
|
+
out+="$(tr '[:lower:]' '[:upper:]' <<< "${w:0:1}")${w:1}"
|
|
190
|
+
done
|
|
191
|
+
printf '%s' "$out"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_short_name() {
|
|
195
|
+
local description="$1"
|
|
196
|
+
local stop='^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$'
|
|
197
|
+
local clean; clean=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
|
198
|
+
local -a words=()
|
|
199
|
+
local word
|
|
200
|
+
for word in $clean; do
|
|
201
|
+
[ -z "$word" ] && continue
|
|
202
|
+
if ! echo "$word" | grep -qiE "$stop"; then
|
|
203
|
+
[ ${#word} -ge 3 ] && words+=("$word")
|
|
204
|
+
fi
|
|
205
|
+
done
|
|
206
|
+
local max=3 result="" count=0
|
|
207
|
+
[ ${#words[@]} -eq 4 ] && max=4
|
|
208
|
+
for word in "${words[@]}"; do
|
|
209
|
+
[ $count -ge $max ] && break
|
|
210
|
+
[ -n "$result" ] && result+="-"
|
|
211
|
+
result+="$word"; count=$((count+1))
|
|
212
|
+
done
|
|
213
|
+
[ -z "$result" ] && result="feature"
|
|
214
|
+
printf '%s' "$result"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ───────────────────────────────── scaffold ────────────────────────────────
|
|
219
|
+
cmd_scaffold() {
|
|
220
|
+
local feature_dir_arg="" allow_existing=false
|
|
221
|
+
while [ $# -gt 0 ]; do
|
|
222
|
+
case "$1" in
|
|
223
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
224
|
+
--allow-existing) allow_existing=true; shift ;;
|
|
225
|
+
--help|-h) echo "scaffold --feature-dir <path> [--allow-existing]"; return 0 ;;
|
|
226
|
+
*) echo "scaffold: unknown arg '$1'" >&2; return 1 ;;
|
|
227
|
+
esac
|
|
228
|
+
done
|
|
229
|
+
[ -n "$feature_dir_arg" ] || { echo "scaffold: --feature-dir is required" >&2; return 1; }
|
|
230
|
+
|
|
231
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
232
|
+
local feature_dir="$feature_dir_arg"
|
|
233
|
+
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
|
|
234
|
+
local spec_file="$feature_dir/spec.md"
|
|
235
|
+
|
|
236
|
+
local dir_existed=false; [ -d "$feature_dir" ] && dir_existed=true
|
|
237
|
+
if [ "$dir_existed" = true ] && [ "$allow_existing" != true ]; then
|
|
238
|
+
echo "scaffold: '$feature_dir' already exists (use --allow-existing)" >&2; return 1
|
|
239
|
+
fi
|
|
240
|
+
mkdir -p "$feature_dir"
|
|
241
|
+
local created=true; [ "$dir_existed" = true ] && created=false
|
|
242
|
+
|
|
243
|
+
local template="" spec_created=false
|
|
244
|
+
if [ ! -f "$spec_file" ]; then
|
|
245
|
+
template=$(resolve_template "spec-template" "$repo_root") || true
|
|
246
|
+
if [ -n "$template" ] && [ -f "$template" ]; then cp "$template" "$spec_file"; else touch "$spec_file"; fi
|
|
247
|
+
spec_created=true
|
|
248
|
+
fi
|
|
249
|
+
_persist_feature_json "$repo_root" "$feature_dir"
|
|
250
|
+
|
|
251
|
+
printf '{"feature_dir":"%s","spec_file":"%s","template":"%s","created":%s,"spec_created":%s}\n' \
|
|
252
|
+
"$(json_escape "$feature_dir")" "$(json_escape "$spec_file")" \
|
|
253
|
+
"$(json_escape "$template")" "$created" "$spec_created"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# ─────────────────────────────── resolve-hooks ─────────────────────────────
|
|
257
|
+
# Parse .aw-spec/extensions.yml (no PyYAML dependency) and emit the executable
|
|
258
|
+
# hooks for a phase. Governance rules applied here (the agent only dispatches):
|
|
259
|
+
# - drop hooks with enabled: false (absent enabled => enabled)
|
|
260
|
+
# - drop hooks with a non-empty condition (leave to HookExecutor)
|
|
261
|
+
# - normalize command dots to hyphens for the slash form
|
|
262
|
+
cmd_resolve_hooks() {
|
|
263
|
+
local -a phases=()
|
|
264
|
+
while [ $# -gt 0 ]; do
|
|
265
|
+
case "$1" in
|
|
266
|
+
--phase) phases+=("$2"); shift 2 ;;
|
|
267
|
+
--help|-h) echo "resolve-hooks --phase <name> [--phase <name> ...] (multiple phases are merged + deduped by command)"; return 0 ;;
|
|
268
|
+
*) echo "resolve-hooks: unknown arg '$1'" >&2; return 1 ;;
|
|
269
|
+
esac
|
|
270
|
+
done
|
|
271
|
+
[ ${#phases[@]} -gt 0 ] || { echo "resolve-hooks: at least one --phase is required" >&2; return 1; }
|
|
272
|
+
|
|
273
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
274
|
+
local ext="$repo_root/.aw-spec/extensions.yml"
|
|
275
|
+
local phases_csv; phases_csv=$(IFS=,; echo "${phases[*]}")
|
|
276
|
+
if [ ! -f "$ext" ]; then
|
|
277
|
+
printf '{"phases":%s,"hooks":[]}\n' "$(json_str_array "${phases[@]}")"; return 0
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
281
|
+
AW_EXT="$ext" AW_PHASES="$phases_csv" python3 - <<'PY'
|
|
282
|
+
import json, os
|
|
283
|
+
path = os.environ["AW_EXT"]
|
|
284
|
+
phases = [p for p in os.environ["AW_PHASES"].split(",") if p]
|
|
285
|
+
lines = open(path, encoding="utf-8").read().splitlines()
|
|
286
|
+
|
|
287
|
+
def indent(s): return len(s) - len(s.lstrip(" "))
|
|
288
|
+
|
|
289
|
+
def parse_phase(phase):
|
|
290
|
+
# locate "hooks:" then "<phase>:" then its list items (targeted parser for the
|
|
291
|
+
# known extensions.yml shape; avoids a YAML dependency). List items ("- ") may
|
|
292
|
+
# sit at the SAME indent as their parent key.
|
|
293
|
+
i, n = 0, len(lines); hooks = []
|
|
294
|
+
while i < n:
|
|
295
|
+
if lines[i].rstrip() == "hooks:":
|
|
296
|
+
base = indent(lines[i]); i += 1
|
|
297
|
+
while i < n and (lines[i].strip() == "" or indent(lines[i]) > base):
|
|
298
|
+
st0 = lines[i].strip()
|
|
299
|
+
if st0.rstrip(":") == phase and st0.endswith(":") and not st0.startswith("- "):
|
|
300
|
+
ph_ind = indent(lines[i]); i += 1; cur = None
|
|
301
|
+
while i < n:
|
|
302
|
+
l = lines[i]
|
|
303
|
+
if l.strip() == "": i += 1; continue
|
|
304
|
+
ind = indent(l); st = l.strip()
|
|
305
|
+
if ind < ph_ind: break
|
|
306
|
+
if ind == ph_ind and st.endswith(":") and not st.startswith("- "): break
|
|
307
|
+
if st.startswith("- "):
|
|
308
|
+
if cur is not None: hooks.append(cur)
|
|
309
|
+
cur = {}; st = st[2:]
|
|
310
|
+
if cur is not None and ":" in st and not st.startswith("- "):
|
|
311
|
+
k, _, v = st.partition(":"); cur[k.strip()] = v.strip()
|
|
312
|
+
i += 1
|
|
313
|
+
if cur is not None: hooks.append(cur)
|
|
314
|
+
break
|
|
315
|
+
else: i += 1
|
|
316
|
+
break
|
|
317
|
+
i += 1
|
|
318
|
+
return hooks
|
|
319
|
+
|
|
320
|
+
def truthy_false(v): return str(v).lower() == "false"
|
|
321
|
+
def empty_cond(v): return v is None or str(v).strip().lower() in ("", "null", "~")
|
|
322
|
+
|
|
323
|
+
out, seen = [], set()
|
|
324
|
+
for phase in phases:
|
|
325
|
+
for h in parse_phase(phase):
|
|
326
|
+
if truthy_false(h.get("enabled", "true")): continue
|
|
327
|
+
if not empty_cond(h.get("condition")): continue
|
|
328
|
+
cmd = h.get("command", "")
|
|
329
|
+
if cmd in seen: continue # dedupe by command across phases
|
|
330
|
+
seen.add(cmd)
|
|
331
|
+
out.append({
|
|
332
|
+
"extension": h.get("extension", ""),
|
|
333
|
+
"command": cmd,
|
|
334
|
+
"slash": "/" + cmd.replace(".", "-") if cmd else "",
|
|
335
|
+
"optional": str(h.get("optional", "false")).lower() == "true",
|
|
336
|
+
"description": h.get("description", ""),
|
|
337
|
+
"prompt": h.get("prompt", ""),
|
|
338
|
+
})
|
|
339
|
+
print(json.dumps({"phases": phases, "hooks": out}))
|
|
340
|
+
PY
|
|
341
|
+
else
|
|
342
|
+
echo "resolve-hooks: python3 unavailable; skipping hook resolution" >&2
|
|
343
|
+
printf '{"phases":%s,"hooks":[]}\n' "$(json_str_array "${phases[@]}")"
|
|
344
|
+
fi
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# ─────────────────────────────── create-checklist ──────────────────────────
|
|
348
|
+
cmd_create_checklist() {
|
|
349
|
+
local feature_dir_arg="" feature_name="" kind="master" change_dir_arg=""
|
|
350
|
+
while [ $# -gt 0 ]; do
|
|
351
|
+
case "$1" in
|
|
352
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
353
|
+
--feature-name) feature_name="$2"; shift 2 ;;
|
|
354
|
+
--kind) kind="$2"; shift 2 ;;
|
|
355
|
+
--change-dir) change_dir_arg="$2"; shift 2 ;;
|
|
356
|
+
--help|-h) echo "create-checklist --feature-dir <path> [--feature-name <name>] [--kind master|delta] [--change-dir <path>]"; return 0 ;;
|
|
357
|
+
*) echo "create-checklist: unknown arg '$1'" >&2; return 1 ;;
|
|
358
|
+
esac
|
|
359
|
+
done
|
|
360
|
+
[ -n "$feature_dir_arg" ] || { echo "create-checklist: --feature-dir is required" >&2; return 1; }
|
|
361
|
+
case "$kind" in
|
|
362
|
+
master|delta) ;;
|
|
363
|
+
*) echo "create-checklist: --kind must be 'master' or 'delta' (got '$kind')" >&2; return 1 ;;
|
|
364
|
+
esac
|
|
365
|
+
|
|
366
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
367
|
+
local feature_dir="$feature_dir_arg"
|
|
368
|
+
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
|
|
369
|
+
[ -n "$feature_name" ] || feature_name="$(basename "$feature_dir")"
|
|
370
|
+
|
|
371
|
+
# WHERE the checklist lives and WHAT it grades are decided together here, and both
|
|
372
|
+
# are reported back, so a caller binds the pair from ONE call and can never point a
|
|
373
|
+
# checklist at the wrong document. master -> the feature's spec.md; delta -> the
|
|
374
|
+
# change's delta-spec.md (checklist sits beside it, inside changes/<cr-id>/).
|
|
375
|
+
local checklist_dir target_file cr_id=""
|
|
376
|
+
if [ "$kind" = delta ]; then
|
|
377
|
+
[ -n "$change_dir_arg" ] || { echo "create-checklist: --change-dir is required with --kind delta" >&2; return 1; }
|
|
378
|
+
local change_dir="$change_dir_arg"
|
|
379
|
+
[[ "$change_dir" != /* ]] && change_dir="$repo_root/$change_dir"
|
|
380
|
+
cr_id="$(basename "$change_dir")"
|
|
381
|
+
checklist_dir="$change_dir/checklists"
|
|
382
|
+
target_file="$change_dir/delta-spec.md"
|
|
383
|
+
else
|
|
384
|
+
checklist_dir="$feature_dir/checklists"
|
|
385
|
+
target_file="$feature_dir/spec.md"
|
|
386
|
+
fi
|
|
387
|
+
local checklist_file="$checklist_dir/requirements.md"
|
|
388
|
+
mkdir -p "$checklist_dir"
|
|
389
|
+
|
|
390
|
+
local created=true; [ -f "$checklist_file" ] && created=false
|
|
391
|
+
local today; today="$(date +%Y-%m-%d)"
|
|
392
|
+
|
|
393
|
+
if [ "$kind" = delta ]; then
|
|
394
|
+
cat > "$checklist_file" <<EOF
|
|
395
|
+
# Delta-Spec Quality Checklist: $feature_name — $cr_id
|
|
396
|
+
|
|
397
|
+
**Purpose**: Validate the CHANGE's delta-spec before proceeding to design
|
|
398
|
+
**Created**: $today
|
|
399
|
+
**Change**: [delta-spec.md](../delta-spec.md)
|
|
400
|
+
|
|
401
|
+
## Content Quality
|
|
402
|
+
|
|
403
|
+
- [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
|
|
404
|
+
- [ ] Focused on user value and business needs
|
|
405
|
+
- [ ] Written for non-technical stakeholders
|
|
406
|
+
- [ ] Change summary states what is changing and why
|
|
407
|
+
|
|
408
|
+
## Delta Completeness
|
|
409
|
+
|
|
410
|
+
- [ ] Every item sits under ADDED, MODIFIED or REMOVED (an empty section states None)
|
|
411
|
+
- [ ] MODIFIED items state both sides (was -> now)
|
|
412
|
+
- [ ] New behaviour is a user story (US-n) with priority, Given/When/Then acceptance, and an independent test
|
|
413
|
+
- [ ] New or changed system requirements keep EARS form
|
|
414
|
+
- [ ] Requirements are testable and unambiguous
|
|
415
|
+
- [ ] No [NEEDS CLARIFICATION] markers remain
|
|
416
|
+
|
|
417
|
+
## Domain Lift (Constraints / Classification / Contracts)
|
|
418
|
+
|
|
419
|
+
- [ ] Constraint / NFR changes are captured as CN- items, or the section states None
|
|
420
|
+
- [ ] Data sensitivity or exposure changes are captured as DC- items, or the section states None
|
|
421
|
+
- [ ] External interoperability changes are captured as IC- items at contract level, or the section states None
|
|
422
|
+
|
|
423
|
+
## Merge Readiness
|
|
424
|
+
|
|
425
|
+
- [ ] Header is complete: aggregate, change type, base version
|
|
426
|
+
- [ ] Impact lists the affected ids (FR / BR / CN / DC / IC / US / SC)
|
|
427
|
+
- [ ] Regression risk names the existing behaviour that must still hold
|
|
428
|
+
- [ ] Scoped to THIS change — the unchanged master is neither restated here nor edited
|
|
429
|
+
|
|
430
|
+
## Notes
|
|
431
|
+
|
|
432
|
+
- This checklist grades the DELTA DOCUMENT only. Accuracy against the master spec
|
|
433
|
+
(id continuity, conflicts with existing requirements) is checked downstream by
|
|
434
|
+
/aw-spec-orch analyze-delta-design.
|
|
435
|
+
- Items marked incomplete require delta-spec updates before /aw-spec-orch design
|
|
436
|
+
EOF
|
|
437
|
+
else
|
|
438
|
+
cat > "$checklist_file" <<EOF
|
|
439
|
+
# Specification Quality Checklist: $feature_name
|
|
440
|
+
|
|
441
|
+
**Purpose**: Validate specification completeness and quality before proceeding to design
|
|
442
|
+
**Created**: $today
|
|
443
|
+
**Feature**: [spec.md](../spec.md)
|
|
444
|
+
|
|
445
|
+
## Content Quality
|
|
446
|
+
|
|
447
|
+
- [ ] No implementation mechanics (languages, frameworks, code/adapter design; interop obligations named as contracts are fine)
|
|
448
|
+
- [ ] Focused on user value and business needs
|
|
449
|
+
- [ ] Written for non-technical stakeholders
|
|
450
|
+
- [ ] All mandatory sections completed
|
|
451
|
+
|
|
452
|
+
## Requirement Completeness
|
|
453
|
+
|
|
454
|
+
- [ ] No [NEEDS CLARIFICATION] markers remain
|
|
455
|
+
- [ ] Requirements are testable and unambiguous
|
|
456
|
+
- [ ] Success criteria are measurable
|
|
457
|
+
- [ ] Success criteria are technology-agnostic (no implementation details)
|
|
458
|
+
- [ ] All acceptance scenarios are defined
|
|
459
|
+
- [ ] Edge cases are identified
|
|
460
|
+
- [ ] Scope is clearly bounded
|
|
461
|
+
- [ ] Dependencies and assumptions identified
|
|
462
|
+
|
|
463
|
+
## Domain Lift (Constraints / Classification / Contracts)
|
|
464
|
+
|
|
465
|
+
- [ ] Domain-derived constraints are captured in Constraints & NFRs (CN-), or the section states None
|
|
466
|
+
- [ ] Data sensitivity and exposure rules are captured in Data Classification (DC-), or the section states None
|
|
467
|
+
- [ ] External interoperability obligations are captured as Integration Contracts (IC-) at contract level, or the section states None
|
|
468
|
+
|
|
469
|
+
## Feature Readiness
|
|
470
|
+
|
|
471
|
+
- [ ] All functional requirements have clear acceptance criteria
|
|
472
|
+
- [ ] User scenarios cover primary flows
|
|
473
|
+
- [ ] Feature meets measurable outcomes defined in Success Criteria
|
|
474
|
+
- [ ] No implementation details leak into specification
|
|
475
|
+
|
|
476
|
+
## Notes
|
|
477
|
+
|
|
478
|
+
- Items marked incomplete require spec updates before /aw-spec-orch design
|
|
479
|
+
EOF
|
|
480
|
+
fi
|
|
481
|
+
|
|
482
|
+
printf '{"checklist_file":"%s","kind":"%s","target":"%s","created":%s}\n' \
|
|
483
|
+
"$(json_escape "$checklist_file")" "$(json_escape "$kind")" \
|
|
484
|
+
"$(json_escape "$target_file")" "$created"
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# ─────────────────────────────── list-domains ──────────────────────────────
|
|
488
|
+
cmd_list_domains() {
|
|
489
|
+
while [ $# -gt 0 ]; do
|
|
490
|
+
case "$1" in
|
|
491
|
+
--help|-h) echo "list-domains"; return 0 ;;
|
|
492
|
+
*) echo "list-domains: unknown arg '$1'" >&2; return 1 ;;
|
|
493
|
+
esac
|
|
494
|
+
done
|
|
495
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
496
|
+
local base="$repo_root/docs/domainspecs"
|
|
497
|
+
local -a domains=()
|
|
498
|
+
if [ -d "$base" ]; then
|
|
499
|
+
local d
|
|
500
|
+
for d in "$base"/*/; do
|
|
501
|
+
[ -d "$d" ] || continue
|
|
502
|
+
domains+=("$(basename "$d")")
|
|
503
|
+
done
|
|
504
|
+
fi
|
|
505
|
+
printf '{"domains":%s}\n' "$(json_str_array "${domains[@]}")"
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
# ─────────────────────────────── set-domain ────────────────────────────────
|
|
509
|
+
cmd_set_domain() {
|
|
510
|
+
local domain=""
|
|
511
|
+
while [ $# -gt 0 ]; do
|
|
512
|
+
case "$1" in
|
|
513
|
+
--domain) domain="$2"; shift 2 ;;
|
|
514
|
+
--help|-h) echo "set-domain --domain <name>"; return 0 ;;
|
|
515
|
+
*) echo "set-domain: unknown arg '$1'" >&2; return 1 ;;
|
|
516
|
+
esac
|
|
517
|
+
done
|
|
518
|
+
[ -n "$domain" ] || { echo "set-domain: --domain is required" >&2; return 1; }
|
|
519
|
+
|
|
520
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
521
|
+
local map_rel="docs/domainspecs/$domain/comandrulemap.md"
|
|
522
|
+
local map_exists=false
|
|
523
|
+
[ -f "$repo_root/$map_rel" ] && map_exists=true
|
|
524
|
+
|
|
525
|
+
mkdir -p "$repo_root/.aw-spec"
|
|
526
|
+
local dj="$repo_root/.aw-spec/domain.json"
|
|
527
|
+
if command -v jq >/dev/null 2>&1; then
|
|
528
|
+
jq -cn --arg d "$domain" '{domain:$d}' > "$dj"
|
|
529
|
+
else
|
|
530
|
+
printf '{"domain":"%s"}\n' "$(json_escape "$domain")" > "$dj"
|
|
531
|
+
fi
|
|
532
|
+
|
|
533
|
+
printf '{"domain":"%s","map_path":"%s","map_exists":%s,"written":true}\n' \
|
|
534
|
+
"$(json_escape "$domain")" "$(json_escape "$map_rel")" "$map_exists"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
# ─────────────────────────────── list-features ─────────────────────────────
|
|
538
|
+
# Enumerate the aggregates a user can pick as the active feature. Two sources,
|
|
539
|
+
# MERGED (deduped by feature name, case-insensitive):
|
|
540
|
+
# 1. specs/ sub-folders — features already scaffolded/adopted (greenfield or
|
|
541
|
+
# brownfield-post-adopt); annotated with spec + design docs + version.
|
|
542
|
+
# 2. the OKF bundle's Aggregate concepts — the BROWNFIELD FEATURE MAP onboard
|
|
543
|
+
# reverse-engineers (the discovered aggregates NOT yet under specs/). This is
|
|
544
|
+
# why select-feature can offer real code features in a brownfield repo before
|
|
545
|
+
# anything is adopted, instead of an empty specs/ list.
|
|
546
|
+
# On greenfield (no bundle) source 2 is empty → identical to the old specs-only
|
|
547
|
+
# behaviour. `select_options` = merged names + (an injected --preselect that is not
|
|
548
|
+
# yet a feature) + a "create new" sentinel, so select-feature can offer add-new and
|
|
549
|
+
# honour a direct-argument fast path with ONE list.
|
|
550
|
+
cmd_list_features() {
|
|
551
|
+
local preselect="" bundle=""
|
|
552
|
+
while [ $# -gt 0 ]; do
|
|
553
|
+
case "$1" in
|
|
554
|
+
--preselect) preselect="$2"; shift 2 ;;
|
|
555
|
+
--bundle) bundle="$2"; shift 2 ;;
|
|
556
|
+
--help|-h) echo "list-features [--preselect <name>] [--bundle <dir>] -> {features[], names[], count, select_options[], sentinel}"; return 0 ;;
|
|
557
|
+
*) echo "list-features: unknown arg '$1'" >&2; return 1 ;;
|
|
558
|
+
esac
|
|
559
|
+
done
|
|
560
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
561
|
+
[ -n "$bundle" ] || bundle="knowledge"
|
|
562
|
+
case "$bundle" in /*) ;; *) bundle="$repo_root/$bundle" ;; esac
|
|
563
|
+
# Best-effort OKF aggregate list from the shared python core (single source of
|
|
564
|
+
# truth for OKF parsing; the ps1 twin calls the SAME script). Empty on greenfield.
|
|
565
|
+
local okf_json="{}" invpy="$SCRIPT_DIR/../python/aw-inventory.py"
|
|
566
|
+
if [ -d "$bundle" ] && [ -f "$invpy" ] && command -v python3 >/dev/null 2>&1; then
|
|
567
|
+
okf_json=$(python3 "$invpy" list-aggregates --bundle "$bundle" 2>/dev/null) || okf_json="{}"
|
|
568
|
+
fi
|
|
569
|
+
AW_SPECS="$repo_root/specs" AW_PRESELECT="$preselect" AW_OKF="$okf_json" python3 - <<'PY'
|
|
570
|
+
import json, os, glob
|
|
571
|
+
base = os.environ["AW_SPECS"]
|
|
572
|
+
pre = os.environ.get("AW_PRESELECT", "").strip()
|
|
573
|
+
try:
|
|
574
|
+
okf = json.loads(os.environ.get("AW_OKF", "{}") or "{}")
|
|
575
|
+
except Exception:
|
|
576
|
+
okf = {}
|
|
577
|
+
sentinel = "+ Create a new feature"
|
|
578
|
+
feats, names = [], []
|
|
579
|
+
seen = set() # case-insensitive dedup key across both sources
|
|
580
|
+
if os.path.isdir(base):
|
|
581
|
+
for name in sorted(os.listdir(base)):
|
|
582
|
+
d = os.path.join(base, name)
|
|
583
|
+
if not os.path.isdir(d):
|
|
584
|
+
continue
|
|
585
|
+
ver, active_change = "", ""
|
|
586
|
+
avf = os.path.join(d, "active_version.json")
|
|
587
|
+
if os.path.isfile(avf):
|
|
588
|
+
try:
|
|
589
|
+
av = json.load(open(avf, encoding="utf-8")) or {}
|
|
590
|
+
ver = av.get("version", ""); active_change = av.get("active_change", "")
|
|
591
|
+
except Exception: pass
|
|
592
|
+
names.append(name); seen.add(name.lower())
|
|
593
|
+
feats.append({
|
|
594
|
+
"name": name,
|
|
595
|
+
"dir": "specs/" + name,
|
|
596
|
+
"source": "spec",
|
|
597
|
+
"has_spec": os.path.isfile(os.path.join(d, "spec.md")),
|
|
598
|
+
# `design*.md`, NOT `design.*.md`: the second dot made this miss the single
|
|
599
|
+
# `design.md` the framework actually produces (it only matched the retired
|
|
600
|
+
# design.domain.md / design.technical.md family). Informational count only.
|
|
601
|
+
"design_docs": len(glob.glob(os.path.join(d, "design*.md"))),
|
|
602
|
+
"version": ver,
|
|
603
|
+
"active_change": active_change,
|
|
604
|
+
})
|
|
605
|
+
# Merge OKF-discovered aggregates NOT already scaffolded under specs/ (a specs/
|
|
606
|
+
# folder is the adopted/created form of the same aggregate — it wins, no dup).
|
|
607
|
+
for agg in (okf.get("aggregates") or []):
|
|
608
|
+
nm = (agg.get("name") or "").strip()
|
|
609
|
+
if not nm or nm.lower() in seen:
|
|
610
|
+
continue
|
|
611
|
+
names.append(nm); seen.add(nm.lower())
|
|
612
|
+
feats.append({
|
|
613
|
+
"name": nm,
|
|
614
|
+
"dir": "specs/" + nm, # candidate slice (not yet created)
|
|
615
|
+
"source": "okf",
|
|
616
|
+
"has_spec": False,
|
|
617
|
+
"adopted": False,
|
|
618
|
+
"concept_path": agg.get("concept_path", ""),
|
|
619
|
+
"design_docs": 0,
|
|
620
|
+
"version": "",
|
|
621
|
+
"active_change": "",
|
|
622
|
+
})
|
|
623
|
+
opts = list(names)
|
|
624
|
+
# Inject a not-yet-existing --preselect so `preselect_from: USER_INPUT` can
|
|
625
|
+
# create-or-select it without an extra round trip (the direct-arg fast path).
|
|
626
|
+
if pre and pre.lower() not in seen:
|
|
627
|
+
opts.append(pre)
|
|
628
|
+
opts.append(sentinel)
|
|
629
|
+
print(json.dumps({"features": feats, "names": names, "count": len(names),
|
|
630
|
+
"select_options": opts, "sentinel": sentinel}))
|
|
631
|
+
PY
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
# ─────────────────────────────── set-feature ───────────────────────────────
|
|
635
|
+
# Create-or-select an aggregate under specs/ and persist it as the active feature
|
|
636
|
+
# (.aw-spec/feature.json). feature = aggregate: the name is normalized to a
|
|
637
|
+
# PascalCase identifier (e.g. "order management" -> OrderManagement). Idempotent:
|
|
638
|
+
# selecting an existing feature just re-points feature.json; a new one is created.
|
|
639
|
+
cmd_set_feature() {
|
|
640
|
+
local raw=""
|
|
641
|
+
while [ $# -gt 0 ]; do
|
|
642
|
+
case "$1" in
|
|
643
|
+
--feature) raw="$2"; shift 2 ;;
|
|
644
|
+
--help|-h) echo "set-feature --feature <name> (create-or-select the aggregate under specs/)"; return 0 ;;
|
|
645
|
+
*) echo "set-feature: unknown arg '$1'" >&2; return 1 ;;
|
|
646
|
+
esac
|
|
647
|
+
done
|
|
648
|
+
[ -n "$raw" ] || { echo "set-feature: --feature is required" >&2; return 1; }
|
|
649
|
+
|
|
650
|
+
# Normalize to a PascalCase aggregate identifier (feature = aggregate).
|
|
651
|
+
local cleaned; cleaned=$(echo "$raw" | sed -E 's/[^A-Za-z0-9]+/ /g; s/^ +| +$//g')
|
|
652
|
+
local feature; feature=$(_pascal "$cleaned")
|
|
653
|
+
if ! [[ "$feature" =~ ^[A-Za-z][A-Za-z0-9]*$ ]]; then
|
|
654
|
+
echo "set-feature: '$raw' is not a valid feature name (need a letter-led alphanumeric aggregate, e.g. Order)" >&2
|
|
655
|
+
return 1
|
|
656
|
+
fi
|
|
657
|
+
|
|
658
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
659
|
+
local feature_dir_rel="specs/$feature"
|
|
660
|
+
local feature_dir="$repo_root/$feature_dir_rel"
|
|
661
|
+
local existed=false; [ -d "$feature_dir" ] && existed=true
|
|
662
|
+
mkdir -p "$feature_dir"
|
|
663
|
+
_persist_feature_json "$repo_root" "$feature_dir_rel"
|
|
664
|
+
|
|
665
|
+
local created=true; [ "$existed" = true ] && created=false
|
|
666
|
+
local spec_exists=false; [ -f "$feature_dir/spec.md" ] && spec_exists=true
|
|
667
|
+
printf '{"feature":"%s","feature_dir":"%s","spec_file":"%s","existed":%s,"created":%s,"spec_exists":%s}\n' \
|
|
668
|
+
"$(json_escape "$feature")" "$(json_escape "$feature_dir_rel")" \
|
|
669
|
+
"$(json_escape "$feature_dir_rel/spec.md")" "$existed" "$created" "$spec_exists"
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# ═══════════════════════════ active_version.json ════════════════════════════
|
|
673
|
+
# PER-FEATURE state at <feature_dir>/active_version.json:
|
|
674
|
+
# {feature, version, active_change, updated}
|
|
675
|
+
# Distinct from the GLOBAL .aw-spec/feature.json (which only SELECTS the active
|
|
676
|
+
# feature). This is the single source of truth for a feature's version number and
|
|
677
|
+
# its open change — the CR that spans cr-setup (opens) -> cr-archive (closes).
|
|
678
|
+
# "Every applied change bumps" => the close op bumps minor ONLY when a change is
|
|
679
|
+
# open (the initial bootstrap build stays v1.0; it is the baseline, not a change).
|
|
680
|
+
# The open/close OPS are reached solely through cr-setup / cr-archive; the former
|
|
681
|
+
# standalone `version-open` / `version-close` subcommands were unused and removed.
|
|
682
|
+
_resolve_feature_dir() { # $1 = --feature-dir arg (may be empty), $2 = repo_root; prints ABS dir or fails
|
|
683
|
+
local arg="$1" repo_root="$2" fd="$1"
|
|
684
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
685
|
+
[ -n "$fd" ] || return 1
|
|
686
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
687
|
+
printf '%s' "$fd"
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
# Shared reader/mutator: op in {read,init,open,close}. Prints one JSON object.
|
|
691
|
+
# op=open enforces the single-active-change guard: opening a DIFFERENT change while
|
|
692
|
+
# one is already open fails (exit 3, ok:false) unless force ("1") is passed — this
|
|
693
|
+
# stops a second concurrent CR on the same aggregate from silently overwriting the
|
|
694
|
+
# first. (Concurrency is ACROSS features; changes to one feature are serialized.)
|
|
695
|
+
_active_version() { # $1 = abs feature_dir, $2 = op, $3 = cr_id (open only), $4 = force ("1" overrides open-guard)
|
|
696
|
+
AW_FD="$1" AW_OP="$2" AW_CR="${3:-}" AW_FORCE="${4:-}" python3 - <<'PY'
|
|
697
|
+
import json, os, re, datetime, sys
|
|
698
|
+
fd = os.environ["AW_FD"]; op = os.environ["AW_OP"]; cr = os.environ.get("AW_CR", "")
|
|
699
|
+
force = os.environ.get("AW_FORCE", "")
|
|
700
|
+
path = os.path.join(fd, "active_version.json")
|
|
701
|
+
feature = os.path.basename(fd.rstrip("/"))
|
|
702
|
+
def now(): return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
703
|
+
def load():
|
|
704
|
+
if os.path.isfile(path):
|
|
705
|
+
try: return json.load(open(path, encoding="utf-8")), True
|
|
706
|
+
except Exception: pass
|
|
707
|
+
return {"feature": feature, "version": "v1.0", "active_change": "", "updated": now()}, False
|
|
708
|
+
def save(d):
|
|
709
|
+
d["updated"] = now()
|
|
710
|
+
open(path, "w", encoding="utf-8").write(json.dumps(d, indent=2) + "\n")
|
|
711
|
+
def bump(v):
|
|
712
|
+
m = re.match(r'v?(\d+)(?:\.(\d+))?', (v or "v1.0").strip())
|
|
713
|
+
maj = int(m.group(1)) if m else 1
|
|
714
|
+
mnr = int(m.group(2)) if (m and m.group(2)) else 0
|
|
715
|
+
return "v%d.%d" % (maj, mnr + 1)
|
|
716
|
+
|
|
717
|
+
d, existed = load()
|
|
718
|
+
d.setdefault("feature", feature); d.setdefault("version", "v1.0"); d.setdefault("active_change", "")
|
|
719
|
+
out = {"feature": d["feature"], "version": d["version"], "active_change": d.get("active_change", ""),
|
|
720
|
+
"exists": existed, "path": "active_version.json", "ok": True}
|
|
721
|
+
if op == "read":
|
|
722
|
+
pass
|
|
723
|
+
elif op == "init":
|
|
724
|
+
if not existed: save(d)
|
|
725
|
+
out["created"] = (not existed)
|
|
726
|
+
elif op == "open":
|
|
727
|
+
cur = d.get("active_change", "")
|
|
728
|
+
if cur and cur != cr and force != "1": # a DIFFERENT change is already open -> refuse
|
|
729
|
+
out["ok"] = False
|
|
730
|
+
out["error"] = ("change '%s' is already open on this feature; finish it "
|
|
731
|
+
"(finalize-delta -> analyze-slice -> close-delta) before opening "
|
|
732
|
+
"'%s', or pass --force" % (cur, cr))
|
|
733
|
+
out["conflict"] = cur; out["requested"] = cr; out["has_change"] = True
|
|
734
|
+
print(json.dumps(out)); sys.exit(3)
|
|
735
|
+
d["active_change"] = cr; save(d)
|
|
736
|
+
out["active_change"] = cr; out["opened"] = cr
|
|
737
|
+
if cur and cur == cr: out["reopened"] = True # idempotent re-open of the same CR
|
|
738
|
+
elif op == "close":
|
|
739
|
+
prev = d["version"]; closed = d.get("active_change", "")
|
|
740
|
+
if closed: # a real change was open -> materialized -> bump
|
|
741
|
+
d["version"] = bump(prev)
|
|
742
|
+
d["active_change"] = ""; save(d)
|
|
743
|
+
out["version"] = d["version"]; out["active_change"] = ""
|
|
744
|
+
out["previous"] = prev; out["closed_change"] = closed
|
|
745
|
+
out["has_change"] = bool(out.get("active_change", "")) # clean branch signal for design
|
|
746
|
+
# What a close from THIS state would produce. One bump() authority, read by BOTH
|
|
747
|
+
# finalize-delta (to write the right number into the Change History rows it appends)
|
|
748
|
+
# and close-delta (which actually performs the bump and stamps the two headers), so
|
|
749
|
+
# the two stages can never disagree about the target. Equals `version` when no change
|
|
750
|
+
# is open, mirroring close's "only bump if a change was really open" guard.
|
|
751
|
+
out["next_version"] = bump(out["version"]) if out.get("active_change", "") else out["version"]
|
|
752
|
+
print(json.dumps(out))
|
|
753
|
+
PY
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
cmd_version_read() { _version_cmd "read" "$@"; }
|
|
757
|
+
cmd_version_init() { _version_cmd "init" "$@"; }
|
|
758
|
+
_version_cmd() { # $1 = op (read|init), then --feature-dir arg
|
|
759
|
+
local op="$1"; shift
|
|
760
|
+
local fd_arg=""
|
|
761
|
+
while [ $# -gt 0 ]; do
|
|
762
|
+
case "$1" in
|
|
763
|
+
--feature-dir) fd_arg="$2"; shift 2 ;;
|
|
764
|
+
--help|-h) echo "version-$op [--feature-dir <path>]"; return 0 ;;
|
|
765
|
+
*) echo "version-$op: unknown arg '$1'" >&2; return 1 ;;
|
|
766
|
+
esac
|
|
767
|
+
done
|
|
768
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
769
|
+
local fd; fd=$(_resolve_feature_dir "$fd_arg" "$repo_root") || { echo "version-$op: no feature set and no --feature-dir" >&2; return 1; }
|
|
770
|
+
_active_version "$fd" "$op"
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
# ─────────────────────────────── feature-paths ─────────────────────────────
|
|
776
|
+
cmd_feature_paths() {
|
|
777
|
+
while [ $# -gt 0 ]; do
|
|
778
|
+
case "$1" in
|
|
779
|
+
--help|-h) echo "feature-paths"; return 0 ;;
|
|
780
|
+
*) echo "feature-paths: unknown arg '$1'" >&2; return 1 ;;
|
|
781
|
+
esac
|
|
782
|
+
done
|
|
783
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
784
|
+
local fd; fd=$(read_feature_json_feature_directory "$repo_root")
|
|
785
|
+
if [ -z "$fd" ]; then
|
|
786
|
+
echo "feature-paths: no feature set (run /aw-spec-orch select-feature first)" >&2
|
|
787
|
+
return 1
|
|
788
|
+
fi
|
|
789
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
790
|
+
local feature; feature=$(basename "$fd") # the aggregate name (feature = specs/<Feature>)
|
|
791
|
+
# The two-document model: spec.md + design.md ONLY. plan.md / tasks.md belonged to
|
|
792
|
+
# the retired /aw-spec-plan + /aw-spec-tasks phases and had zero consumers.
|
|
793
|
+
local spec="$fd/spec.md" design="$fd/design.md"
|
|
794
|
+
local spec_exists=false; [ -f "$spec" ] && spec_exists=true
|
|
795
|
+
local design_exists=false; [ -f "$design" ] && design_exists=true
|
|
796
|
+
printf '{"feature":"%s","feature_dir":"%s","spec_file":"%s","design_file":"%s","spec_exists":%s,"design_exists":%s}\n' \
|
|
797
|
+
"$(json_escape "$feature")" "$(json_escape "$fd")" "$(json_escape "$spec")" "$(json_escape "$design")" \
|
|
798
|
+
"$spec_exists" "$design_exists"
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
# ────────────────────────── checklist-read / -apply ────────────────────────
|
|
802
|
+
# Parse/toggle GitHub task-list checkboxes outside code fences. Python3 does the
|
|
803
|
+
# parsing (robust, fence-aware); toggling changes ONLY the [ ]/[x] marker.
|
|
804
|
+
cmd_checklist_read() {
|
|
805
|
+
local file=""
|
|
806
|
+
while [ $# -gt 0 ]; do
|
|
807
|
+
case "$1" in
|
|
808
|
+
--file) file="$2"; shift 2 ;;
|
|
809
|
+
--help|-h) echo "checklist-read --file <path>"; return 0 ;;
|
|
810
|
+
*) echo "checklist-read: unknown arg '$1'" >&2; return 1 ;;
|
|
811
|
+
esac
|
|
812
|
+
done
|
|
813
|
+
[ -n "$file" ] || { echo "checklist-read: --file required" >&2; return 1; }
|
|
814
|
+
AW_FILE="$file" python3 - <<'PY'
|
|
815
|
+
import json, os
|
|
816
|
+
f = os.environ["AW_FILE"]
|
|
817
|
+
if not os.path.exists(f):
|
|
818
|
+
print(json.dumps({"exists": False, "total": 0, "checked": 0, "items": []})); raise SystemExit
|
|
819
|
+
items, n, fence = [], 0, False
|
|
820
|
+
for line in open(f, encoding="utf-8").read().splitlines():
|
|
821
|
+
s = line.strip()
|
|
822
|
+
if s.startswith("```"): fence = not fence; continue
|
|
823
|
+
if fence: continue
|
|
824
|
+
st = line.lstrip()
|
|
825
|
+
if st[:3] in ("- [",) and len(st) >= 5 and st[3] in " xX" and st[4] == "]":
|
|
826
|
+
n += 1
|
|
827
|
+
items.append({"n": n, "checked": st[3] in "xX", "text": st[5:].strip()})
|
|
828
|
+
print(json.dumps({"exists": True, "total": len(items),
|
|
829
|
+
"checked": sum(1 for i in items if i["checked"]), "items": items}))
|
|
830
|
+
PY
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
cmd_checklist_apply() {
|
|
834
|
+
local file="" results="{}"
|
|
835
|
+
while [ $# -gt 0 ]; do
|
|
836
|
+
case "$1" in
|
|
837
|
+
--file) file="$2"; shift 2 ;;
|
|
838
|
+
--results) results="$2"; shift 2 ;;
|
|
839
|
+
--help|-h) echo "checklist-apply --file <path> --results <json>"; return 0 ;;
|
|
840
|
+
*) echo "checklist-apply: unknown arg '$1'" >&2; return 1 ;;
|
|
841
|
+
esac
|
|
842
|
+
done
|
|
843
|
+
[ -n "$file" ] || { echo "checklist-apply: --file required" >&2; return 1; }
|
|
844
|
+
AW_FILE="$file" AW_RESULTS="$results" python3 - <<'PY'
|
|
845
|
+
import json, os, re
|
|
846
|
+
f = os.environ["AW_FILE"]
|
|
847
|
+
if not os.path.exists(f):
|
|
848
|
+
print(json.dumps({"exists": False, "before_checked": 0, "after_checked": 0,
|
|
849
|
+
"total": 0, "newly_passing": [], "regressions": []})); raise SystemExit
|
|
850
|
+
try:
|
|
851
|
+
results = {int(k): bool(v) for k, v in json.loads(os.environ["AW_RESULTS"] or "{}").items()}
|
|
852
|
+
except Exception:
|
|
853
|
+
results = {}
|
|
854
|
+
lines = open(f, encoding="utf-8").read().splitlines(keepends=True)
|
|
855
|
+
n, fence = 0, False
|
|
856
|
+
before = after = 0
|
|
857
|
+
newly, regress = [], []
|
|
858
|
+
out = []
|
|
859
|
+
for line in lines:
|
|
860
|
+
s = line.strip()
|
|
861
|
+
if s.startswith("```"):
|
|
862
|
+
fence = not fence; out.append(line); continue
|
|
863
|
+
m = re.match(r'^(\s*- \[)([ xX])(\].*)$', line.rstrip("\n")) if not fence else None
|
|
864
|
+
if m:
|
|
865
|
+
n += 1
|
|
866
|
+
cur = m.group(2) in "xX"
|
|
867
|
+
before += 1 if cur else 0
|
|
868
|
+
desired = results.get(n, cur)
|
|
869
|
+
if desired != cur:
|
|
870
|
+
if desired: newly.append(n)
|
|
871
|
+
else: regress.append(n)
|
|
872
|
+
marker = "x" if desired else " "
|
|
873
|
+
nl = "\n" if line.endswith("\n") else ""
|
|
874
|
+
out.append(f"{m.group(1)}{marker}{m.group(3)}{nl}")
|
|
875
|
+
after += 1 if desired else 0
|
|
876
|
+
else:
|
|
877
|
+
out.append(line)
|
|
878
|
+
open(f, "w", encoding="utf-8").write("".join(out))
|
|
879
|
+
print(json.dumps({"exists": True, "before_checked": before, "after_checked": after,
|
|
880
|
+
"total": n, "newly_passing": newly, "regressions": regress}))
|
|
881
|
+
PY
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
# ─────────────────────────────── detect-project ────────────────────────────
|
|
887
|
+
# Project Structure Gate: is there a scaffolded app (not just framework files)?
|
|
888
|
+
cmd_detect_project() {
|
|
889
|
+
while [ $# -gt 0 ]; do
|
|
890
|
+
case "$1" in
|
|
891
|
+
--help|-h) echo "detect-project"; return 0 ;;
|
|
892
|
+
*) echo "detect-project: unknown arg '$1'" >&2; return 1 ;;
|
|
893
|
+
esac
|
|
894
|
+
done
|
|
895
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
896
|
+
local -a markers=()
|
|
897
|
+
# .NET solution/project (ignore framework dirs)
|
|
898
|
+
while IFS= read -r m; do [ -n "$m" ] && markers+=("$m"); done < <(
|
|
899
|
+
find "$repo_root" \( -path "$repo_root/.aw-spec" -o -path "$repo_root/docs" -o -path "$repo_root/specs" -o -path "$repo_root/.git" -o -name node_modules \) -prune -o \
|
|
900
|
+
\( -name "*.sln" -o -name "*.csproj" \) -print 2>/dev/null | sed "s#^$repo_root/##" | head -20 )
|
|
901
|
+
# Other ecosystems at repo root
|
|
902
|
+
local f
|
|
903
|
+
for f in package.json pom.xml build.gradle build.gradle.kts go.mod Cargo.toml pyproject.toml; do
|
|
904
|
+
[ -f "$repo_root/$f" ] && markers+=("$f")
|
|
905
|
+
done
|
|
906
|
+
local scaffolded=false
|
|
907
|
+
[ ${#markers[@]} -gt 0 ] && scaffolded=true
|
|
908
|
+
printf '{"scaffolded":%s,"markers":%s}\n' "$scaffolded" "$(json_str_array "${markers[@]}")"
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
# ──────────────────────────── foundation-list ──────────────────────────────
|
|
913
|
+
# Enumerate available foundations (docs/<Name>/ that carry a real <Name>.pack.txt)
|
|
914
|
+
# so the workflow can let the user PICK one — deterministic, no LLM. An optional
|
|
915
|
+
# --args "<Foundation> <App> [out]" is parsed as a direct-argument fast path:
|
|
916
|
+
# preselect_name is echoed back only when it names a real foundation; preselect_app
|
|
917
|
+
# / out_dir carry any 2nd/3rd tokens through (empty/"." otherwise).
|
|
918
|
+
cmd_foundation_list() {
|
|
919
|
+
local raw=""
|
|
920
|
+
while [ $# -gt 0 ]; do
|
|
921
|
+
case "$1" in
|
|
922
|
+
--args) raw="$2"; shift 2 ;;
|
|
923
|
+
--help|-h) echo "foundation-list [--args '<Foundation> <App> [out]'] -> {foundations[], count, preselect_name, preselect_app, out_dir}"; return 0 ;;
|
|
924
|
+
*) echo "foundation-list: unknown arg '$1'" >&2; return 1 ;;
|
|
925
|
+
esac
|
|
926
|
+
done
|
|
927
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
928
|
+
local -a foundations=()
|
|
929
|
+
if [ -d "$repo_root/docs" ]; then
|
|
930
|
+
local d name
|
|
931
|
+
for d in "$repo_root/docs"/*/; do
|
|
932
|
+
[ -d "$d" ] || continue
|
|
933
|
+
name="$(basename "$d")"
|
|
934
|
+
[ -f "$d$name.pack.txt" ] && foundations+=("$name") # the primary pack (excludes Reference-only dirs)
|
|
935
|
+
done
|
|
936
|
+
fi
|
|
937
|
+
# Optional direct-argument fast path: "<FoundationName> <AppName> [OutputDir]".
|
|
938
|
+
local pre_name="" pre_app="" out_dir="."
|
|
939
|
+
if [ -n "$raw" ]; then
|
|
940
|
+
# shellcheck disable=SC2086
|
|
941
|
+
set -- $raw
|
|
942
|
+
[ -n "${1:-}" ] && pre_name="$1"; [ -n "${2:-}" ] && pre_app="$2"; [ -n "${3:-}" ] && out_dir="$3"
|
|
943
|
+
fi
|
|
944
|
+
# Only preselect a name that actually names an available foundation.
|
|
945
|
+
local match="" f
|
|
946
|
+
for f in "${foundations[@]}"; do [ "$f" = "$pre_name" ] && match="$pre_name"; done
|
|
947
|
+
printf '{"foundations":%s,"count":%s,"preselect_name":"%s","preselect_app":"%s","out_dir":"%s"}\n' \
|
|
948
|
+
"$(json_str_array "${foundations[@]}")" "${#foundations[@]}" \
|
|
949
|
+
"$(json_escape "$match")" "$(json_escape "$pre_app")" "$(json_escape "$out_dir")"
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
# ─────────────────────────── foundation-locate ─────────────────────────────
|
|
953
|
+
# Parse/validate args, locate the pack + unpacker, read the pack header (bounded).
|
|
954
|
+
cmd_foundation_locate() {
|
|
955
|
+
local name="" app="" out="." raw=""
|
|
956
|
+
while [ $# -gt 0 ]; do
|
|
957
|
+
case "$1" in
|
|
958
|
+
--name) name="$2"; shift 2 ;;
|
|
959
|
+
--app) app="$2"; shift 2 ;;
|
|
960
|
+
--out) out="$2"; shift 2 ;;
|
|
961
|
+
--args) raw="$2"; shift 2 ;;
|
|
962
|
+
--help|-h) echo "foundation-locate (--args '<F> <A> [out]' | --name <F> --app <A> [--out <dir>])"; return 0 ;;
|
|
963
|
+
*) echo "foundation-locate: unknown arg '$1'" >&2; return 1 ;;
|
|
964
|
+
esac
|
|
965
|
+
done
|
|
966
|
+
# positional parse: "<FoundationName> <AppName> [OutputDir]"
|
|
967
|
+
if [ -n "$raw" ]; then
|
|
968
|
+
# shellcheck disable=SC2086
|
|
969
|
+
set -- $raw
|
|
970
|
+
[ -n "$1" ] && name="$1"; [ -n "$2" ] && app="$2"; [ -n "$3" ] && out="$3"
|
|
971
|
+
fi
|
|
972
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
973
|
+
# available = docs subdirs containing a *.pack.txt
|
|
974
|
+
local -a available=()
|
|
975
|
+
if [ -d "$repo_root/docs" ]; then
|
|
976
|
+
local d
|
|
977
|
+
for d in "$repo_root/docs"/*/; do
|
|
978
|
+
[ -d "$d" ] || continue
|
|
979
|
+
ls "$d"*.pack.txt >/dev/null 2>&1 && available+=("$(basename "$d")")
|
|
980
|
+
done
|
|
981
|
+
fi
|
|
982
|
+
_fnd_fail() { printf '{"ok":false,"reason":"%s","available":%s}\n' "$(json_escape "$1")" "$(json_str_array "${available[@]}")"; }
|
|
983
|
+
if [ -z "$name" ] || [ -z "$app" ]; then
|
|
984
|
+
_fnd_fail "Usage: /aw-spec-foundation <FoundationName> <AppName> [OutputDirectory]"; return 0
|
|
985
|
+
fi
|
|
986
|
+
local fdir="$repo_root/docs/$name"
|
|
987
|
+
if [ ! -d "$fdir" ]; then _fnd_fail "Foundation '$name' not found (expected docs/$name/)"; return 0; fi
|
|
988
|
+
local pack="$fdir/$name.pack.txt"
|
|
989
|
+
if [ ! -f "$pack" ]; then _fnd_fail "Foundation '$name' has no bundle yet (expected $pack). Build it via docs/$name/PACKAGING.md."; return 0; fi
|
|
990
|
+
# runner + unpacker
|
|
991
|
+
local runner unpacker
|
|
992
|
+
if command -v pwsh >/dev/null 2>&1 && [ -f "$fdir/unpack-foundation.ps1" ]; then
|
|
993
|
+
runner="pwsh"; unpacker="docs/$name/unpack-foundation.ps1"
|
|
994
|
+
elif [ -f "$fdir/unpack-foundation.sh" ]; then
|
|
995
|
+
runner="sh"; unpacker="docs/$name/unpack-foundation.sh"
|
|
996
|
+
else
|
|
997
|
+
emit_fail "No unpacker found in docs/$name/ (unpack-foundation.ps1 or .sh)"; return 0
|
|
998
|
+
fi
|
|
999
|
+
# bounded header read (never load the pack body)
|
|
1000
|
+
local header sentinel file_count
|
|
1001
|
+
header=$(grep -m 30 '^#' "$pack" 2>/dev/null || true)
|
|
1002
|
+
sentinel=$(printf '%s\n' "$header" | grep -iE 'sentinel' | head -1 | sed -E 's/.*[sS]entinel[^A-Za-z0-9_.]*//; s/[^A-Za-z0-9_.].*$//')
|
|
1003
|
+
file_count=$(printf '%s\n' "$header" | grep -iE 'file' | grep -oE '[0-9]+' | head -1)
|
|
1004
|
+
[ -n "$file_count" ] || file_count="0"
|
|
1005
|
+
|
|
1006
|
+
printf '{"ok":true,"reason":"","name":"%s","app":"%s","out_dir":"%s","pack_file":"%s","unpacker":"%s","runner":"%s","sentinel":"%s","file_count":%s,"available":%s}\n' \
|
|
1007
|
+
"$(json_escape "$name")" "$(json_escape "$app")" "$(json_escape "$out")" "$(json_escape "docs/$name/$name.pack.txt")" \
|
|
1008
|
+
"$(json_escape "$unpacker")" "$runner" "$(json_escape "$sentinel")" "$file_count" "$(json_str_array "${available[@]}")"
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
# ─────────────────────────── foundation-unpack ─────────────────────────────
|
|
1012
|
+
# Run the OS-selected unpacker for the foundation (or reference) pack.
|
|
1013
|
+
cmd_foundation_unpack() {
|
|
1014
|
+
local name="" app="" out="." dry=false force="no" reference=false
|
|
1015
|
+
while [ $# -gt 0 ]; do
|
|
1016
|
+
case "$1" in
|
|
1017
|
+
--name) name="$2"; shift 2 ;;
|
|
1018
|
+
--app) app="$2"; shift 2 ;;
|
|
1019
|
+
--out) out="$2"; shift 2 ;;
|
|
1020
|
+
--dry-run) dry=true; shift ;;
|
|
1021
|
+
--force) force="$2"; shift 2 ;;
|
|
1022
|
+
--reference) reference=true; shift ;;
|
|
1023
|
+
--help|-h) echo "foundation-unpack --name <F> --app <A> --out <d> [--dry-run] [--force <yes|no>] [--reference]"; return 0 ;;
|
|
1024
|
+
*) echo "foundation-unpack: unknown arg '$1'" >&2; return 1 ;;
|
|
1025
|
+
esac
|
|
1026
|
+
done
|
|
1027
|
+
[ -n "$name" ] && [ -n "$app" ] || { echo "foundation-unpack: --name and --app required" >&2; return 1; }
|
|
1028
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1029
|
+
local fdir="$repo_root/docs/$name"
|
|
1030
|
+
local packbase="$name" script_stem="unpack-foundation"
|
|
1031
|
+
if [ "$reference" = true ]; then packbase="$name.Reference"; script_stem="unpack-reference-foundation"; fi
|
|
1032
|
+
local pack="$fdir/$packbase.pack.txt"
|
|
1033
|
+
if [ ! -f "$pack" ]; then printf '{"ok":false,"mode":"%s","written":0,"has_collisions":false,"stdout":"pack not found: %s"}\n' "$([ "$dry" = true ] && echo dry || echo real)" "$(json_escape "$packbase.pack.txt")"; return 0; fi
|
|
1034
|
+
|
|
1035
|
+
# pick runner
|
|
1036
|
+
local -a cmd=()
|
|
1037
|
+
if command -v pwsh >/dev/null 2>&1 && [ -f "$fdir/$script_stem.ps1" ]; then
|
|
1038
|
+
cmd=(pwsh -NoProfile -File "$fdir/$script_stem.ps1" -PackFile "$pack" -TargetAppName "$app" -OutDir "$out")
|
|
1039
|
+
[ "$dry" = true ] && cmd+=(-DryRun)
|
|
1040
|
+
[ "$force" = "yes" ] && cmd+=(-Force)
|
|
1041
|
+
[ "$reference" = true ] && [ "$force" != "yes" ] && cmd+=(-Force) # reference always forces
|
|
1042
|
+
elif [ -f "$fdir/$script_stem.sh" ]; then
|
|
1043
|
+
cmd=(bash "$fdir/$script_stem.sh" --pack-file "$pack" --target-app-name "$app" --out-dir "$out")
|
|
1044
|
+
[ "$dry" = true ] && cmd+=(--dry-run)
|
|
1045
|
+
{ [ "$force" = "yes" ] || [ "$reference" = true ]; } && cmd+=(--force)
|
|
1046
|
+
else
|
|
1047
|
+
printf '{"ok":false,"mode":"real","written":0,"has_collisions":false,"stdout":"no unpacker script for %s"}\n' "$(json_escape "$name")"; return 0
|
|
1048
|
+
fi
|
|
1049
|
+
|
|
1050
|
+
local outp rc
|
|
1051
|
+
rc=0
|
|
1052
|
+
outp=$("${cmd[@]}" 2>&1) || rc=$? # set -e safe: a failing unpacker (dry-run collisions) must still yield JSON
|
|
1053
|
+
# unpackers say "Unpacked N file(s) to …" / "DryRun: N file(s) would be written …"
|
|
1054
|
+
local written; written=$(printf '%s' "$outp" | grep -oiE '[0-9]+ +file' | grep -oE '[0-9]+' | head -1)
|
|
1055
|
+
[ -n "$written" ] || written=0
|
|
1056
|
+
local has_coll=false
|
|
1057
|
+
if [ "$dry" = true ] && [ $rc -ne 0 ]; then has_coll=true; fi
|
|
1058
|
+
printf '{"ok":%s,"mode":"%s","written":%s,"has_collisions":%s,"stdout":"%s"}\n' \
|
|
1059
|
+
"$([ $rc -eq 0 ] && echo true || echo false)" "$([ "$dry" = true ] && echo dry || echo real)" \
|
|
1060
|
+
"$written" "$has_coll" "$(json_escape "$(printf '%s' "$outp" | tail -c 800)")"
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
# ─────────────────────── foundation-stash-dbcontext ────────────────────────
|
|
1064
|
+
# Copy the current ApplicationDbContext.cs to a .foundation.bak sibling before a
|
|
1065
|
+
# reference unpack overwrites it (so the two can be merged afterward).
|
|
1066
|
+
cmd_foundation_stash_dbcontext() {
|
|
1067
|
+
local out="."
|
|
1068
|
+
while [ $# -gt 0 ]; do
|
|
1069
|
+
case "$1" in
|
|
1070
|
+
--out) out="$2"; shift 2 ;;
|
|
1071
|
+
--help|-h) echo "foundation-stash-dbcontext --out <dir>"; return 0 ;;
|
|
1072
|
+
*) echo "foundation-stash-dbcontext: unknown arg '$1'" >&2; return 1 ;;
|
|
1073
|
+
esac
|
|
1074
|
+
done
|
|
1075
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1076
|
+
[[ "$out" != /* ]] && out="$repo_root/$out"
|
|
1077
|
+
local ctx; ctx=$(find "$out/src" -name "ApplicationDbContext.cs" 2>/dev/null | head -1)
|
|
1078
|
+
if [ -z "$ctx" ]; then printf '{"found":false,"dbcontext":"","backup":""}\n'; return 0; fi
|
|
1079
|
+
local bak="$ctx.foundation.bak"
|
|
1080
|
+
cp "$ctx" "$bak"
|
|
1081
|
+
printf '{"found":true,"dbcontext":"%s","backup":"%s"}\n' "$(json_escape "$ctx")" "$(json_escape "$bak")"
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
# ─────────────────────────── foundation-verify ─────────────────────────────
|
|
1085
|
+
# Scan the output tree for leftover sentinel tokens (should be none).
|
|
1086
|
+
cmd_foundation_verify() {
|
|
1087
|
+
local out="." sentinel=""
|
|
1088
|
+
while [ $# -gt 0 ]; do
|
|
1089
|
+
case "$1" in
|
|
1090
|
+
--out) out="$2"; shift 2 ;;
|
|
1091
|
+
--sentinel) sentinel="$2"; shift 2 ;;
|
|
1092
|
+
--help|-h) echo "foundation-verify --out <dir> --sentinel <s>"; return 0 ;;
|
|
1093
|
+
*) echo "foundation-verify: unknown arg '$1'" >&2; return 1 ;;
|
|
1094
|
+
esac
|
|
1095
|
+
done
|
|
1096
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1097
|
+
[[ "$out" != /* ]] && out="$repo_root/$out"
|
|
1098
|
+
local -a residue=()
|
|
1099
|
+
if [ -n "$sentinel" ] && [ -d "$out" ]; then
|
|
1100
|
+
# scan the generated tree only — never the bundle/framework dirs (the
|
|
1101
|
+
# .pack.txt legitimately contains the sentinel).
|
|
1102
|
+
while IFS= read -r m; do [ -n "$m" ] && residue+=("$m"); done < <(
|
|
1103
|
+
grep -rIl --exclude-dir=.git --exclude-dir=docs --exclude-dir=.aw-spec \
|
|
1104
|
+
--exclude-dir=specs --exclude-dir=node_modules -- "$sentinel" "$out" 2>/dev/null \
|
|
1105
|
+
| sed "s#^$repo_root/##" | head -50 )
|
|
1106
|
+
fi
|
|
1107
|
+
printf '{"clean":%s,"residue":%s}\n' "$([ ${#residue[@]} -eq 0 ] && echo true || echo false)" "$(json_str_array "${residue[@]}")"
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
# ─────────────────────────────── setup-constitution ────────────────────────
|
|
1111
|
+
# Ensure the split constitution files exist (copy from templates if missing):
|
|
1112
|
+
# .aw-spec/memory/constitution.technical.md (from constitution-technical-template)
|
|
1113
|
+
# .aw-spec/memory/constitution.domain.md (from constitution-domain-template)
|
|
1114
|
+
# The composed .aw-spec/memory/constitution.md is produced by `compose-constitution`
|
|
1115
|
+
# after both halves are authored. Each half is authored + verified independently.
|
|
1116
|
+
cmd_setup_constitution() {
|
|
1117
|
+
while [ $# -gt 0 ]; do
|
|
1118
|
+
case "$1" in
|
|
1119
|
+
--help|-h) echo "setup-constitution"; return 0 ;;
|
|
1120
|
+
*) echo "setup-constitution: unknown arg '$1'" >&2; return 1 ;;
|
|
1121
|
+
esac
|
|
1122
|
+
done
|
|
1123
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1124
|
+
mkdir -p "$repo_root/.aw-spec/memory"
|
|
1125
|
+
local tech="$repo_root/.aw-spec/memory/constitution.technical.md"
|
|
1126
|
+
local dom="$repo_root/.aw-spec/memory/constitution.domain.md"
|
|
1127
|
+
local tech_created=false dom_created=false
|
|
1128
|
+
if [ ! -f "$tech" ]; then
|
|
1129
|
+
local tpl; tpl=$(resolve_template "constitution-technical-template" "$repo_root") || true
|
|
1130
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$tech"; else touch "$tech"; fi
|
|
1131
|
+
tech_created=true
|
|
1132
|
+
fi
|
|
1133
|
+
if [ ! -f "$dom" ]; then
|
|
1134
|
+
local tpl; tpl=$(resolve_template "constitution-domain-template" "$repo_root") || true
|
|
1135
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$dom"; else touch "$dom"; fi
|
|
1136
|
+
dom_created=true
|
|
1137
|
+
fi
|
|
1138
|
+
printf '{"constitution_technical_file":"%s","constitution_domain_file":"%s","technical_created":%s,"domain_created":%s}\n' \
|
|
1139
|
+
"$(json_escape "$tech")" "$(json_escape "$dom")" "$tech_created" "$dom_created"
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
# ─────────────────────────────── compose-constitution ──────────────────────
|
|
1143
|
+
# Deterministically concatenate the technical + domain halves into the composed
|
|
1144
|
+
# .aw-spec/memory/constitution.md (human/compat view; NOT loaded by the pipeline
|
|
1145
|
+
# phases — they load the halves directly). No model. If the domain half is absent
|
|
1146
|
+
# or empty (technical-only project), the composed file mirrors the technical half.
|
|
1147
|
+
cmd_compose_constitution() {
|
|
1148
|
+
while [ $# -gt 0 ]; do
|
|
1149
|
+
case "$1" in
|
|
1150
|
+
--help|-h) echo "compose-constitution"; return 0 ;;
|
|
1151
|
+
*) echo "compose-constitution: unknown arg '$1'" >&2; return 1 ;;
|
|
1152
|
+
esac
|
|
1153
|
+
done
|
|
1154
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1155
|
+
local tech="$repo_root/.aw-spec/memory/constitution.technical.md"
|
|
1156
|
+
local dom="$repo_root/.aw-spec/memory/constitution.domain.md"
|
|
1157
|
+
local out="$repo_root/.aw-spec/memory/constitution.md"
|
|
1158
|
+
[ -f "$tech" ] || { printf '{"composed_file":"%s","ok":false,"domain_included":false,"issues":["technical half not found"]}\n' "$(json_escape "$out")"; return 0; }
|
|
1159
|
+
# Include the domain half only if it is authored — non-empty AND free of residual
|
|
1160
|
+
# [ALL_CAPS] template placeholders (a technical-only project leaves it as a raw template).
|
|
1161
|
+
local include_domain=false
|
|
1162
|
+
if [ -f "$dom" ] && [ -s "$dom" ] && ! grep -qE '\[[A-Z0-9]+(_[A-Z0-9]+)+\]' "$dom"; then
|
|
1163
|
+
include_domain=true
|
|
1164
|
+
fi
|
|
1165
|
+
{
|
|
1166
|
+
printf '<!-- COMPOSED — generated by `aw-spec-actions compose-constitution`. Do not edit here.\n'
|
|
1167
|
+
printf ' Source of truth: constitution.technical.md + constitution.domain.md (edit those).\n'
|
|
1168
|
+
printf ' Pipeline phases load the halves directly; this composed view is for humans/compat. -->\n\n'
|
|
1169
|
+
cat "$tech"
|
|
1170
|
+
if [ "$include_domain" = true ]; then
|
|
1171
|
+
printf '\n\n---\n\n'
|
|
1172
|
+
cat "$dom"
|
|
1173
|
+
fi
|
|
1174
|
+
} > "$out"
|
|
1175
|
+
printf '{"composed_file":"%s","ok":true,"domain_included":%s,"issues":[]}\n' "$(json_escape "$out")" "$include_domain"
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
# ─────────────────────────── constitution-verify ───────────────────────────
|
|
1179
|
+
# Deterministic lint of a generated constitution (NO LLM): residual
|
|
1180
|
+
# [ALL_CAPS_WITH_UNDERSCORE] placeholder tokens, body-vs-Sync-Impact-Report version
|
|
1181
|
+
# agreement, and non-ISO dates. Consumed by the constitution skill's bounded
|
|
1182
|
+
# self-correction loop. ok=false ⇒ the loop asks the model to patch.
|
|
1183
|
+
cmd_constitution_verify() {
|
|
1184
|
+
local file=""
|
|
1185
|
+
while [ $# -gt 0 ]; do
|
|
1186
|
+
case "$1" in
|
|
1187
|
+
--file) file="$2"; shift 2 ;;
|
|
1188
|
+
--help|-h) echo "constitution-verify --file <path>"; return 0 ;;
|
|
1189
|
+
*) echo "constitution-verify: unknown arg '$1'" >&2; return 1 ;;
|
|
1190
|
+
esac
|
|
1191
|
+
done
|
|
1192
|
+
[ -n "$file" ] || { echo "constitution-verify: --file required" >&2; return 1; }
|
|
1193
|
+
AW_FILE="$file" python3 - <<'PY'
|
|
1194
|
+
import json, os, re
|
|
1195
|
+
f = os.environ["AW_FILE"]
|
|
1196
|
+
if not os.path.exists(f):
|
|
1197
|
+
print(json.dumps({"ok": False, "exists": False,
|
|
1198
|
+
"issues": ["constitution file not found"], "placeholders": [],
|
|
1199
|
+
"version_body": None, "version_report": None,
|
|
1200
|
+
"version_match": None, "bad_dates": []})); raise SystemExit
|
|
1201
|
+
text = open(f, encoding="utf-8").read()
|
|
1202
|
+
# Strip HTML comments (template guidance + the Sync Impact Report) so they do not
|
|
1203
|
+
# trip the body scan; keep the first block as the report for the version check.
|
|
1204
|
+
m = re.search(r"<!--.*?-->", text, re.S)
|
|
1205
|
+
report = m.group(0) if m else ""
|
|
1206
|
+
body = re.sub(r"<!--.*?-->", "", text, flags=re.S)
|
|
1207
|
+
# 1. Residual ALL_CAPS_WITH_UNDERSCORE placeholder tokens (the template convention).
|
|
1208
|
+
placeholders = sorted(set(re.findall(r"\[([A-Z0-9]+(?:_[A-Z0-9]+)+)\]", body)))
|
|
1209
|
+
# 2. Version: labeled body version vs the report's "new" version.
|
|
1210
|
+
def labeled(s):
|
|
1211
|
+
mm = re.search(r"(?im)version[^0-9\n]{0,20}v?(\d+\.\d+\.\d+)", s)
|
|
1212
|
+
return mm.group(1) if mm else None
|
|
1213
|
+
version_body = labeled(body)
|
|
1214
|
+
arrow = re.search(r"(\d+\.\d+\.\d+)\s*(?:->|→|=>)\s*(\d+\.\d+\.\d+)", report)
|
|
1215
|
+
version_report = arrow.group(2) if arrow else labeled(report)
|
|
1216
|
+
version_match = (version_body == version_report) if (version_body and version_report) else None
|
|
1217
|
+
# 3. Obviously non-ISO dates on ratification/amended/date lines (slash forms,
|
|
1218
|
+
# DD-MM-YYYY, US textual). ISO YYYY-MM-DD never matches these patterns.
|
|
1219
|
+
bad = []
|
|
1220
|
+
for line in body.splitlines():
|
|
1221
|
+
if re.search(r"(?i)ratif|amend|\bdate\b", line):
|
|
1222
|
+
bad += re.findall(r"\b\d{1,2}/\d{1,2}/\d{2,4}\b", line)
|
|
1223
|
+
bad += re.findall(r"\b\d{1,2}-\d{1,2}-\d{4}\b", line)
|
|
1224
|
+
bad += re.findall(r"\b(?:January|February|March|April|May|June|July|August|"
|
|
1225
|
+
r"September|October|November|December)\s+\d{1,2},?\s+\d{4}\b", line)
|
|
1226
|
+
bad = sorted(set(bad))
|
|
1227
|
+
issues = []
|
|
1228
|
+
if placeholders:
|
|
1229
|
+
issues.append("%d unfilled placeholder(s): %s" % (len(placeholders), ", ".join(placeholders)))
|
|
1230
|
+
if version_match is False:
|
|
1231
|
+
issues.append("version mismatch: body %s vs Sync Impact Report %s" % (version_body, version_report))
|
|
1232
|
+
if bad:
|
|
1233
|
+
issues.append("non-ISO date(s) (use YYYY-MM-DD): %s" % ", ".join(bad))
|
|
1234
|
+
ok = (not placeholders) and (version_match is not False) and (not bad)
|
|
1235
|
+
print(json.dumps({"ok": ok, "exists": True, "issues": issues,
|
|
1236
|
+
"placeholders": placeholders, "version_body": version_body,
|
|
1237
|
+
"version_report": version_report, "version_match": version_match,
|
|
1238
|
+
"bad_dates": bad}))
|
|
1239
|
+
PY
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
# ────────────────────────── agent-context-update ───────────────────────────
|
|
1243
|
+
# Run the agent-context extension's updater (refreshes the managed AW-Spec block).
|
|
1244
|
+
cmd_agent_context_update() {
|
|
1245
|
+
local plan=""
|
|
1246
|
+
while [ $# -gt 0 ]; do
|
|
1247
|
+
case "$1" in
|
|
1248
|
+
--plan) plan="$2"; shift 2 ;;
|
|
1249
|
+
--help|-h) echo "agent-context-update [--plan <path>]"; return 0 ;;
|
|
1250
|
+
*) echo "agent-context-update: unknown arg '$1'" >&2; return 1 ;;
|
|
1251
|
+
esac
|
|
1252
|
+
done
|
|
1253
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1254
|
+
local script="$repo_root/.aw-spec/extensions/agent-context/scripts/bash/update-agent-context.sh"
|
|
1255
|
+
if [ ! -f "$script" ]; then
|
|
1256
|
+
printf '{"ok":true,"ran":false,"message":"agent-context extension not installed; nothing to do"}\n'; return 0
|
|
1257
|
+
fi
|
|
1258
|
+
local out rc
|
|
1259
|
+
if [ -n "$plan" ]; then out=$(bash "$script" "$plan" 2>&1); else out=$(bash "$script" 2>&1); fi
|
|
1260
|
+
rc=$?
|
|
1261
|
+
printf '{"ok":%s,"ran":true,"message":"%s"}\n' \
|
|
1262
|
+
"$([ $rc -eq 0 ] && echo true || echo false)" "$(json_escape "$(printf '%s' "$out" | tail -c 400)")"
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
# ─────────────────────────────── list-components ───────────────────────────
|
|
1268
|
+
# Immediate subdirs of docs/components/ that contain an integration.md runbook.
|
|
1269
|
+
cmd_list_components() {
|
|
1270
|
+
while [ $# -gt 0 ]; do
|
|
1271
|
+
case "$1" in
|
|
1272
|
+
--help|-h) echo "list-components"; return 0 ;;
|
|
1273
|
+
*) echo "list-components: unknown arg '$1'" >&2; return 1 ;;
|
|
1274
|
+
esac
|
|
1275
|
+
done
|
|
1276
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1277
|
+
local base="$repo_root/docs/components"
|
|
1278
|
+
local -a comps=()
|
|
1279
|
+
if [ -d "$base" ]; then
|
|
1280
|
+
local d
|
|
1281
|
+
for d in "$base"/*/; do
|
|
1282
|
+
[ -d "$d" ] || continue
|
|
1283
|
+
[ -f "$d/integration.md" ] && comps+=("$(basename "$d")")
|
|
1284
|
+
done
|
|
1285
|
+
fi
|
|
1286
|
+
printf '{"components":%s}\n' "$(json_str_array "${comps[@]}")"
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
# ─────────────────────────────── find-host-project ─────────────────────────
|
|
1290
|
+
# Locate the host/startup project by finding Program.cs (never assume a name).
|
|
1291
|
+
cmd_find_host_project() {
|
|
1292
|
+
while [ $# -gt 0 ]; do
|
|
1293
|
+
case "$1" in
|
|
1294
|
+
--help|-h) echo "find-host-project"; return 0 ;;
|
|
1295
|
+
*) echo "find-host-project: unknown arg '$1'" >&2; return 1 ;;
|
|
1296
|
+
esac
|
|
1297
|
+
done
|
|
1298
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1299
|
+
local prog
|
|
1300
|
+
# prefer under src/, ignore framework/build dirs
|
|
1301
|
+
prog=$(find "$repo_root/src" -name "Program.cs" 2>/dev/null | head -1)
|
|
1302
|
+
[ -z "$prog" ] && prog=$(find "$repo_root" \( -path "$repo_root/.aw-spec" -o -path "$repo_root/docs" -o -path "$repo_root/specs" -o -path "$repo_root/.git" -o -name obj -o -name bin \) -prune -o -name "Program.cs" -print 2>/dev/null | head -1)
|
|
1303
|
+
if [ -z "$prog" ]; then printf '{"found":false,"program_cs":"","project_dir":"","csproj":""}\n'; return 0; fi
|
|
1304
|
+
local pdir; pdir=$(dirname "$prog")
|
|
1305
|
+
local csproj; csproj=$(find "$pdir" -maxdepth 1 -name "*.csproj" 2>/dev/null | head -1)
|
|
1306
|
+
printf '{"found":true,"program_cs":"%s","project_dir":"%s","csproj":"%s"}\n' \
|
|
1307
|
+
"$(json_escape "$prog")" "$(json_escape "$pdir")" "$(json_escape "$csproj")"
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
# ─────────────────────────────── component-catalog ──────────────────────────
|
|
1311
|
+
# Deterministic parse of a component's integration.md "Implementation catalog
|
|
1312
|
+
# (selection list)" markdown table (NO LLM). Replaces the extract_catalog llm_query.
|
|
1313
|
+
# Returns {integrable, impl_keys[], implementations[{key,desc}]}.
|
|
1314
|
+
cmd_component_catalog() {
|
|
1315
|
+
local file=""
|
|
1316
|
+
while [ $# -gt 0 ]; do
|
|
1317
|
+
case "$1" in
|
|
1318
|
+
--file) file="$2"; shift 2 ;;
|
|
1319
|
+
--help|-h) echo "component-catalog --file <integration.md>"; return 0 ;;
|
|
1320
|
+
*) echo "component-catalog: unknown arg '$1'" >&2; return 1 ;;
|
|
1321
|
+
esac
|
|
1322
|
+
done
|
|
1323
|
+
[ -n "$file" ] || { echo "component-catalog: --file required" >&2; return 1; }
|
|
1324
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1325
|
+
[[ "$file" != /* ]] && file="$repo_root/$file"
|
|
1326
|
+
AW_FILE="$file" python3 - <<'PY'
|
|
1327
|
+
import json, os, re
|
|
1328
|
+
f = os.environ["AW_FILE"]
|
|
1329
|
+
if not os.path.exists(f):
|
|
1330
|
+
print(json.dumps({"integrable": False, "impl_keys": [], "implementations": []})); raise SystemExit
|
|
1331
|
+
lines = open(f, encoding="utf-8").read().splitlines()
|
|
1332
|
+
start = None
|
|
1333
|
+
for i, ln in enumerate(lines):
|
|
1334
|
+
if re.match(r'^#{1,6}\s', ln) and re.search(r'(?i)implementation catalog|selection list', ln):
|
|
1335
|
+
start = i + 1; break
|
|
1336
|
+
rows = []
|
|
1337
|
+
if start is not None:
|
|
1338
|
+
for ln in lines[start:]:
|
|
1339
|
+
if re.match(r'^#{1,6}\s', ln): # next heading ends the section
|
|
1340
|
+
break
|
|
1341
|
+
s = ln.strip()
|
|
1342
|
+
if s.startswith("|"):
|
|
1343
|
+
rows.append(s)
|
|
1344
|
+
elif rows: # blank/non-table after the table => done
|
|
1345
|
+
break
|
|
1346
|
+
def cells(row):
|
|
1347
|
+
return [c.strip() for c in row.strip().strip("|").split("|")]
|
|
1348
|
+
impl_keys, implementations = [], []
|
|
1349
|
+
if len(rows) >= 2:
|
|
1350
|
+
header = cells(rows[0])
|
|
1351
|
+
kidx = 0
|
|
1352
|
+
for j, h in enumerate(header):
|
|
1353
|
+
if h.strip().lower() == "key":
|
|
1354
|
+
kidx = j; break
|
|
1355
|
+
for row in rows[2:]: # skip header + separator
|
|
1356
|
+
c = cells(row)
|
|
1357
|
+
if not c or all(re.fullmatch(r':?-{2,}:?', (x or '')) for x in c):
|
|
1358
|
+
continue
|
|
1359
|
+
if kidx >= len(c):
|
|
1360
|
+
continue
|
|
1361
|
+
key = c[kidx].strip("` ").strip()
|
|
1362
|
+
if not key:
|
|
1363
|
+
continue
|
|
1364
|
+
parts = []
|
|
1365
|
+
for j, val in enumerate(c):
|
|
1366
|
+
if j == kidx or not val:
|
|
1367
|
+
continue
|
|
1368
|
+
h = header[j] if j < len(header) else ""
|
|
1369
|
+
parts.append("%s: %s" % (h, val) if h else val)
|
|
1370
|
+
impl_keys.append(key)
|
|
1371
|
+
implementations.append({"key": key, "desc": " · ".join(parts)})
|
|
1372
|
+
print(json.dumps({"integrable": len(impl_keys) > 0,
|
|
1373
|
+
"impl_keys": impl_keys, "implementations": implementations}))
|
|
1374
|
+
PY
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
# ─────────────────────────────── component-verify ───────────────────────────
|
|
1378
|
+
# Post-wiring verification (NO LLM decision): (1) `dotnet build` the host project
|
|
1379
|
+
# (graceful skip if the SDK is absent); (2) R0.2a dual-registration guard — when the
|
|
1380
|
+
# host has BOTH AddInfrastructure and AddAspireInfrastructure, assert `--marker`
|
|
1381
|
+
# appears in both. Returns {ok, ran, build_ok, errors[], dual_registration_ok,
|
|
1382
|
+
# fix_target, fix_reason, can_fix} for a bounded fix loop.
|
|
1383
|
+
cmd_component_verify() {
|
|
1384
|
+
local host_dir="" host_csproj="" marker=""
|
|
1385
|
+
while [ $# -gt 0 ]; do
|
|
1386
|
+
case "$1" in
|
|
1387
|
+
--host-dir) host_dir="$2"; shift 2 ;;
|
|
1388
|
+
--host-csproj) host_csproj="$2"; shift 2 ;;
|
|
1389
|
+
--marker) marker="$2"; shift 2 ;;
|
|
1390
|
+
--help|-h) echo "component-verify --host-dir <dir> [--host-csproj <path>] [--marker <token>]"; return 0 ;;
|
|
1391
|
+
*) echo "component-verify: unknown arg '$1'" >&2; return 1 ;;
|
|
1392
|
+
esac
|
|
1393
|
+
done
|
|
1394
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1395
|
+
AW_REPO="$repo_root" AW_HOST_DIR="$host_dir" AW_HOST_CSPROJ="$host_csproj" AW_MARKER="$marker" python3 - <<'PY'
|
|
1396
|
+
import json, os, re, shutil, subprocess
|
|
1397
|
+
repo = os.environ.get("AW_REPO", "")
|
|
1398
|
+
hostd = os.environ.get("AW_HOST_DIR", "")
|
|
1399
|
+
csproj = os.environ.get("AW_HOST_CSPROJ", "")
|
|
1400
|
+
marker = os.environ.get("AW_MARKER", "").strip()
|
|
1401
|
+
def relp(p):
|
|
1402
|
+
try: return os.path.relpath(p, repo).replace("\\", "/") if (repo and p) else p
|
|
1403
|
+
except Exception: return p
|
|
1404
|
+
# --- (1) build ---
|
|
1405
|
+
ran, build_ok, errors, primary = False, True, [], ""
|
|
1406
|
+
if shutil.which("dotnet"):
|
|
1407
|
+
target = csproj if csproj else hostd
|
|
1408
|
+
if target and os.path.exists(target):
|
|
1409
|
+
ran = True
|
|
1410
|
+
proc = subprocess.run(["dotnet", "build", target, "-nologo"],
|
|
1411
|
+
capture_output=True, text=True)
|
|
1412
|
+
seen = set()
|
|
1413
|
+
for line in (proc.stdout + "\n" + proc.stderr).splitlines():
|
|
1414
|
+
if re.search(r':\s*error\s', line):
|
|
1415
|
+
t = line.strip()
|
|
1416
|
+
if t not in seen:
|
|
1417
|
+
seen.add(t); errors.append(t)
|
|
1418
|
+
errors = errors[:20]
|
|
1419
|
+
build_ok = (proc.returncode == 0)
|
|
1420
|
+
if not build_ok and errors:
|
|
1421
|
+
m = re.match(r'^(.*?)\(\d+,\d+\)', errors[0])
|
|
1422
|
+
if m: primary = relp(m.group(1).strip())
|
|
1423
|
+
if not build_ok and not primary and csproj:
|
|
1424
|
+
primary = relp(csproj)
|
|
1425
|
+
# --- (2) R0.2a dual-registration guard ---
|
|
1426
|
+
def defining_files(method):
|
|
1427
|
+
root = hostd if (hostd and os.path.isdir(hostd)) else repo
|
|
1428
|
+
hits = []
|
|
1429
|
+
if not root: return hits
|
|
1430
|
+
pat = re.compile(r'\b' + re.escape(method) + r'\s*\(')
|
|
1431
|
+
for dp, dns, fns in os.walk(root):
|
|
1432
|
+
dns[:] = [d for d in dns if d not in ("bin", "obj", ".git")]
|
|
1433
|
+
for fn in fns:
|
|
1434
|
+
if fn.endswith(".cs"):
|
|
1435
|
+
fp = os.path.join(dp, fn)
|
|
1436
|
+
try: txt = open(fp, encoding="utf-8", errors="ignore").read()
|
|
1437
|
+
except Exception: continue
|
|
1438
|
+
if pat.search(txt): hits.append((fp, txt))
|
|
1439
|
+
return hits
|
|
1440
|
+
infra, aspire = defining_files("AddInfrastructure"), defining_files("AddAspireInfrastructure")
|
|
1441
|
+
dual_app = bool(infra) and bool(aspire)
|
|
1442
|
+
dual_ok, dual_note, dual_fix = True, "", ""
|
|
1443
|
+
def pick_fix(hits):
|
|
1444
|
+
non_prog = [fp for fp, _ in hits if os.path.basename(fp) != "Program.cs"]
|
|
1445
|
+
if non_prog: return relp(non_prog[0])
|
|
1446
|
+
return relp(hits[0][0]) if hits else ""
|
|
1447
|
+
if not dual_app:
|
|
1448
|
+
dual_note = "single registration path (R0.2) — dual-registration check N/A"
|
|
1449
|
+
elif not marker:
|
|
1450
|
+
dual_note = "both AddInfrastructure and AddAspireInfrastructure exist; no marker provided to verify the component is in both"
|
|
1451
|
+
else:
|
|
1452
|
+
in_infra = any(marker in txt for _, txt in infra)
|
|
1453
|
+
in_aspire = any(marker in txt for _, txt in aspire)
|
|
1454
|
+
dual_ok = in_infra and in_aspire
|
|
1455
|
+
if not dual_ok:
|
|
1456
|
+
missing = "AddInfrastructure" if not in_infra else "AddAspireInfrastructure"
|
|
1457
|
+
dual_fix = pick_fix(infra if not in_infra else aspire)
|
|
1458
|
+
dual_note = "marker '%s' missing from %s (R0.2a: register in BOTH paths)" % (marker, missing)
|
|
1459
|
+
else:
|
|
1460
|
+
dual_note = "marker '%s' present in both registration paths" % marker
|
|
1461
|
+
ok = build_ok and dual_ok
|
|
1462
|
+
fix_target, fix_reason = "", ""
|
|
1463
|
+
if not build_ok:
|
|
1464
|
+
fix_target = primary or (relp(csproj) if csproj else "")
|
|
1465
|
+
fix_reason = "build failed: " + " | ".join(errors[:5])
|
|
1466
|
+
elif not dual_ok:
|
|
1467
|
+
fix_target, fix_reason = dual_fix, dual_note
|
|
1468
|
+
print(json.dumps({"ok": ok, "ran": ran, "build_ok": build_ok, "errors": errors,
|
|
1469
|
+
"primary_error_file": primary, "dual_applicable": dual_app,
|
|
1470
|
+
"dual_registration_ok": dual_ok, "dual_note": dual_note,
|
|
1471
|
+
"dual_fix_file": dual_fix, "fix_target": fix_target,
|
|
1472
|
+
"fix_reason": fix_reason, "can_fix": bool(fix_target)}))
|
|
1473
|
+
PY
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
# ─────────────────────────────────── main ──────────────────────────────────
|
|
1477
|
+
sub="${1:-}"; shift || true
|
|
1478
|
+
# ──────────────────────────────── design-scaffold ──────────────────────────
|
|
1479
|
+
# The spec-anchored Design phase: ONE high-level design doc per feature
|
|
1480
|
+
# (design.md) — a tech-agnostic HLD that `implement` GENERATES code from (not a
|
|
1481
|
+
# deterministic extract). See .aw-spec/DESIGN-FORMAT.md + design-template.md.
|
|
1482
|
+
#
|
|
1483
|
+
# The former path-tagged helpers (design-extract / -reconcile / -paths) are GONE.
|
|
1484
|
+
# They belonged to a retired four-document model in which design.{domain,application,
|
|
1485
|
+
# api,infra}.md carried the source inside ```lang path=… fences. Nothing emits that
|
|
1486
|
+
# fence any more, and their glob (`design.*.md`) could not match a single `design.md`
|
|
1487
|
+
# even if something did — so all three had been returning nothing, silently, at exit 0.
|
|
1488
|
+
# The question they answered ("which files is this slice made of") is now answered by
|
|
1489
|
+
# the OKF concept: `aw-xref concept-paths` to read, `concept-upsert --merge` to write.
|
|
1490
|
+
|
|
1491
|
+
# Copy the single HLD design template into the feature dir as design.md (idempotent).
|
|
1492
|
+
cmd_design_scaffold() {
|
|
1493
|
+
local feature_dir_arg="" template_arg="design-template"
|
|
1494
|
+
while [ $# -gt 0 ]; do
|
|
1495
|
+
case "$1" in
|
|
1496
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
1497
|
+
# --template <name>: which template to scaffold (default `design-template`).
|
|
1498
|
+
# In a brownfield repo, /aw-spec-orch onboard GENERATES a code-derived
|
|
1499
|
+
# `design-template` override (resolve_template Priority-1), so the default
|
|
1500
|
+
# already yields the as-built structure for both forward `design` and adopt
|
|
1501
|
+
# (no per-call --template needed). Resolved via resolve_template.
|
|
1502
|
+
--template) template_arg="$2"; shift 2 ;;
|
|
1503
|
+
--help|-h) echo "design-scaffold --feature-dir <path> [--template <name>]"; return 0 ;;
|
|
1504
|
+
*) echo "design-scaffold: unknown arg '$1'" >&2; return 1 ;;
|
|
1505
|
+
esac
|
|
1506
|
+
done
|
|
1507
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1508
|
+
local fd="$feature_dir_arg"
|
|
1509
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
1510
|
+
[ -n "$fd" ] || { echo "design-scaffold: no feature set (run /aw-spec-orch select-feature first) and no --feature-dir" >&2; return 1; }
|
|
1511
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
1512
|
+
mkdir -p "$fd"
|
|
1513
|
+
|
|
1514
|
+
local target="$fd/design.md" created="false" tpl
|
|
1515
|
+
if [ -f "$target" ]; then
|
|
1516
|
+
created="false"
|
|
1517
|
+
else
|
|
1518
|
+
tpl=$(resolve_template "$template_arg" "$repo_root") || true
|
|
1519
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$target"; else touch "$target"; fi
|
|
1520
|
+
created="true"
|
|
1521
|
+
fi
|
|
1522
|
+
# override_active: is a PROJECT-level override installed for this template name?
|
|
1523
|
+
# Computed ALWAYS (independent of the idempotent scaffold skip above) so it is a
|
|
1524
|
+
# stable signal on both the bootstrap AND delta paths. In the brownfield-dev flow,
|
|
1525
|
+
# /aw-spec-orch onboard GENERATES the code-derived `design-template` override — so
|
|
1526
|
+
# override_active=true means the forward `design` phase should author with the
|
|
1527
|
+
# (code-derived) as-built structure, not the greenfield four-layer layout.
|
|
1528
|
+
local override_active="false"
|
|
1529
|
+
[ -f "$repo_root/.aw-spec/templates/overrides/${template_arg}.md" ] && override_active="true"
|
|
1530
|
+
local template_source; template_source=$(resolve_template "$template_arg" "$repo_root") || true
|
|
1531
|
+
printf '{"feature_dir":"%s","design_doc":"%s","created":%s,"override_active":%s,"template":"%s","template_source":"%s"}\n' \
|
|
1532
|
+
"$(json_escape "$fd")" "$(json_escape "$target")" "$created" "$override_active" \
|
|
1533
|
+
"$(json_escape "$template_arg")" "$(json_escape "${template_source:-}")"
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
# ─────────────────────── build / test / migrate / deploy ────────────────────
|
|
1537
|
+
# Detect the project's toolchain, run the phase command, and report structured
|
|
1538
|
+
# status. Tolerant: an unrecognized stack is a no-op pass (wire a hook instead).
|
|
1539
|
+
# The FIX is an LLM step in the workflow; these only run + report.
|
|
1540
|
+
|
|
1541
|
+
cmd_build_compile() {
|
|
1542
|
+
while [ $# -gt 0 ]; do
|
|
1543
|
+
case "$1" in
|
|
1544
|
+
--feature-dir) shift 2 ;;
|
|
1545
|
+
--help|-h) echo "build-compile [--feature-dir <path>]"; return 0 ;;
|
|
1546
|
+
*) echo "build-compile: unknown arg '$1'" >&2; return 1 ;;
|
|
1547
|
+
esac
|
|
1548
|
+
done
|
|
1549
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1550
|
+
local tool="none" ran=false ok=true cmd="" log; log=$(mktemp)
|
|
1551
|
+
if ls "$repo_root"/*.sln >/dev/null 2>&1 || find "$repo_root/src" -maxdepth 4 -name "*.csproj" 2>/dev/null | grep -q .; then
|
|
1552
|
+
tool="dotnet"
|
|
1553
|
+
if command -v dotnet >/dev/null 2>&1; then
|
|
1554
|
+
cmd="dotnet build --nologo"; ran=true
|
|
1555
|
+
if ( cd "$repo_root" && dotnet build --nologo ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
1556
|
+
else ok=false; fi
|
|
1557
|
+
elif [ -f "$repo_root/package.json" ] && command -v npm >/dev/null 2>&1; then
|
|
1558
|
+
tool="npm"; cmd="npm run build --if-present"; ran=true
|
|
1559
|
+
if ( cd "$repo_root" && npm run build --if-present ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
1560
|
+
else
|
|
1561
|
+
tool="none"; ran=false; ok=true
|
|
1562
|
+
fi
|
|
1563
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
1564
|
+
printf '{"ok":%s,"tool":"%s","ran":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
1565
|
+
"$ok" "$(json_escape "$tool")" "$ran" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
cmd_test_run() {
|
|
1569
|
+
# --scope repo (default): run the whole suite — the historic behaviour, every
|
|
1570
|
+
# existing caller is unaffected.
|
|
1571
|
+
# --scope slice : run ONLY the tests whose FullyQualifiedName contains the
|
|
1572
|
+
# feature's aggregate name (feature-dir basename) plus any --also
|
|
1573
|
+
# referrer names — the per-feature integrity scope. .NET only; npm/none
|
|
1574
|
+
# ignore it and run the full suite.
|
|
1575
|
+
local feature_dir="" scope="repo" also=""
|
|
1576
|
+
while [ $# -gt 0 ]; do
|
|
1577
|
+
case "$1" in
|
|
1578
|
+
--feature-dir) feature_dir="$2"; shift 2 ;;
|
|
1579
|
+
--scope) scope="$2"; shift 2 ;;
|
|
1580
|
+
--also) also="$2"; shift 2 ;;
|
|
1581
|
+
--help|-h) echo "test-run [--feature-dir <path>] [--scope slice|repo] [--also <csv>]"; return 0 ;;
|
|
1582
|
+
*) echo "test-run: unknown arg '$1'" >&2; return 1 ;;
|
|
1583
|
+
esac
|
|
1584
|
+
done
|
|
1585
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1586
|
+
|
|
1587
|
+
# For scope=slice, build the token set = feature aggregate name + --also referrers,
|
|
1588
|
+
# each sanitised to an identifier (VSTest filter operators |&()!=~ are illegal) and
|
|
1589
|
+
# de-duplicated. A token that survives sanitising becomes a `FullyQualifiedName~<tok>`
|
|
1590
|
+
# clause; substring match is deliberate — it over-includes (safe for an integrity
|
|
1591
|
+
# fence) rather than risking an under-scoped miss.
|
|
1592
|
+
local slices="" name tok also_sp
|
|
1593
|
+
if [ "$scope" = "slice" ]; then
|
|
1594
|
+
also_sp=$(printf '%s' "$also" | tr ',' ' ')
|
|
1595
|
+
for name in "$(basename "${feature_dir%/}")" $also_sp; do
|
|
1596
|
+
tok=$(printf '%s' "$name" | tr -cd 'A-Za-z0-9._-')
|
|
1597
|
+
case "$tok" in [A-Za-z]*) ;; *) continue ;; esac # must start with a letter
|
|
1598
|
+
case " $slices " in *" $tok "*) continue ;; esac # de-dup
|
|
1599
|
+
slices="${slices:+$slices }$tok"
|
|
1600
|
+
done
|
|
1601
|
+
fi
|
|
1602
|
+
|
|
1603
|
+
local tool="none" ran=false ok=true cmd="" log; log=$(mktemp)
|
|
1604
|
+
local matched=false scoped=false fence_green=false filter="" t2
|
|
1605
|
+
if ls "$repo_root"/*.sln >/dev/null 2>&1 || find "$repo_root" -maxdepth 5 -name "*.csproj" 2>/dev/null | grep -q .; then
|
|
1606
|
+
tool="dotnet"
|
|
1607
|
+
if command -v dotnet >/dev/null 2>&1; then
|
|
1608
|
+
if [ "$scope" = "slice" ]; then
|
|
1609
|
+
if [ -z "$slices" ]; then
|
|
1610
|
+
# scope=slice but nothing valid to filter on — fail loudly, never
|
|
1611
|
+
# silently fall back to a repo-wide run.
|
|
1612
|
+
printf 'scope=slice but no valid filter tokens from --feature-dir/--also\n' >"$log"
|
|
1613
|
+
ok=false; ran=false; cmd="(no filter tokens)"
|
|
1614
|
+
else
|
|
1615
|
+
for t2 in $slices; do filter="${filter:+$filter|}FullyQualifiedName~$t2"; done
|
|
1616
|
+
scoped=true; cmd="dotnet test --nologo --filter \"$filter\""; ran=true
|
|
1617
|
+
if ( cd "$repo_root" && dotnet test --nologo --filter "$filter" ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
1618
|
+
fi
|
|
1619
|
+
else
|
|
1620
|
+
cmd="dotnet test --nologo"; ran=true
|
|
1621
|
+
if ( cd "$repo_root" && dotnet test --nologo ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
1622
|
+
fi
|
|
1623
|
+
else ok=false; fi
|
|
1624
|
+
elif [ -f "$repo_root/package.json" ] && command -v npm >/dev/null 2>&1; then
|
|
1625
|
+
tool="npm"; cmd="npm test"; ran=true
|
|
1626
|
+
if ( cd "$repo_root" && npm test ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
1627
|
+
else
|
|
1628
|
+
tool="none"; ran=false; ok=true
|
|
1629
|
+
fi
|
|
1630
|
+
|
|
1631
|
+
# matched = at least one test was actually selected + run. Read from the LOG, never
|
|
1632
|
+
# the exit code: a .NET --filter that selects ZERO tests still EXITS 0, so trusting
|
|
1633
|
+
# the exit code would report an empty scoped run as green (the vacuous-pass trap).
|
|
1634
|
+
if grep -Eq 'Total:[[:space:]]*[1-9]' "$log" 2>/dev/null; then matched=true; fi
|
|
1635
|
+
|
|
1636
|
+
# fence_green — a green that actually proves something ran.
|
|
1637
|
+
# scoped run → ok AND matched (guards the zero-match vacuous pass)
|
|
1638
|
+
# everything else (repo suite, npm, no test tool, the loud no-token fail) → ok,
|
|
1639
|
+
# preserving the historic pass-through for a project with no/zero tests.
|
|
1640
|
+
if [ "$scoped" = true ]; then
|
|
1641
|
+
if [ "$ok" = true ] && [ "$matched" = true ]; then fence_green=true; fi
|
|
1642
|
+
else
|
|
1643
|
+
fence_green=$ok
|
|
1644
|
+
fi
|
|
1645
|
+
|
|
1646
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
1647
|
+
printf '{"ok":%s,"tool":"%s","ran":%s,"scope":"%s","slices":"%s","scoped":%s,"matched":%s,"fence_green":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
1648
|
+
"$ok" "$(json_escape "$tool")" "$ran" "$(json_escape "$scope")" "$(json_escape "$slices")" \
|
|
1649
|
+
"$scoped" "$matched" "$fence_green" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
cmd_run_migrate() {
|
|
1653
|
+
while [ $# -gt 0 ]; do
|
|
1654
|
+
case "$1" in
|
|
1655
|
+
--feature-dir) shift 2 ;;
|
|
1656
|
+
--help|-h) echo "run-migrate [--feature-dir <path>]"; return 0 ;;
|
|
1657
|
+
*) echo "run-migrate: unknown arg '$1'" >&2; return 1 ;;
|
|
1658
|
+
esac
|
|
1659
|
+
done
|
|
1660
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1661
|
+
local tool="none" ran=false ok=true cmd="" log; log=$(mktemp)
|
|
1662
|
+
local has_dotnet=false
|
|
1663
|
+
if ls "$repo_root"/*.sln >/dev/null 2>&1 || find "$repo_root/src" -maxdepth 4 -name "*.csproj" 2>/dev/null | grep -q .; then has_dotnet=true; fi
|
|
1664
|
+
if [ "$has_dotnet" = true ] && command -v dotnet >/dev/null 2>&1 && ( cd "$repo_root" && dotnet ef --version >/dev/null 2>&1 ); then
|
|
1665
|
+
# Prefer *Service.csproj (API host with ApplicationDbContext); fall back to first
|
|
1666
|
+
# non-AppHost/Web/ServiceDefaults/test project under src/.
|
|
1667
|
+
local project=""
|
|
1668
|
+
project=$(find "$repo_root/src" -maxdepth 3 -name '*.Service.csproj' 2>/dev/null | head -1)
|
|
1669
|
+
if [ -z "$project" ]; then
|
|
1670
|
+
project=$(find "$repo_root/src" -maxdepth 3 -name '*.csproj' 2>/dev/null \
|
|
1671
|
+
| grep -vE 'AppHost|ServiceDefaults|\.Web\.|Tests' | head -1)
|
|
1672
|
+
fi
|
|
1673
|
+
if [ -n "$project" ]; then
|
|
1674
|
+
# Multi-context solutions need an explicit context (ApplicationDbContext is the app model).
|
|
1675
|
+
tool="dotnet-ef"
|
|
1676
|
+
cmd="dotnet ef database update --project $project --context ApplicationDbContext"
|
|
1677
|
+
ran=true
|
|
1678
|
+
if ( cd "$repo_root" && dotnet ef database update --project "$project" --context ApplicationDbContext ) >"$log" 2>&1; then
|
|
1679
|
+
ok=true
|
|
1680
|
+
else
|
|
1681
|
+
ok=false
|
|
1682
|
+
fi
|
|
1683
|
+
else
|
|
1684
|
+
tool="dotnet-ef"; cmd="dotnet ef database update"; ran=true
|
|
1685
|
+
if ( cd "$repo_root" && dotnet ef database update ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
1686
|
+
fi
|
|
1687
|
+
else
|
|
1688
|
+
tool="none"; ran=false; ok=true # no .NET project / migration tool = nothing to run
|
|
1689
|
+
fi
|
|
1690
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
1691
|
+
printf '{"ok":%s,"tool":"%s","ran":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
1692
|
+
"$ok" "$(json_escape "$tool")" "$ran" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
# Deploy is intentionally config-driven: reads .aw-spec/deploy.json {"command": "..."}.
|
|
1696
|
+
cmd_deploy_dispatch() {
|
|
1697
|
+
while [ $# -gt 0 ]; do
|
|
1698
|
+
case "$1" in
|
|
1699
|
+
--help|-h) echo "deploy-dispatch"; return 0 ;;
|
|
1700
|
+
*) echo "deploy-dispatch: unknown arg '$1'" >&2; return 1 ;;
|
|
1701
|
+
esac
|
|
1702
|
+
done
|
|
1703
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1704
|
+
local cfg="$repo_root/.aw-spec/deploy.json"
|
|
1705
|
+
local configured=false ran=false ok=true cmd="" log; log=$(mktemp)
|
|
1706
|
+
if [ -f "$cfg" ] && command -v python3 >/dev/null 2>&1; then
|
|
1707
|
+
cmd=$(AW_CFG="$cfg" python3 -c "import json,os;print(json.load(open(os.environ['AW_CFG'])).get('command',''))" 2>/dev/null || true)
|
|
1708
|
+
fi
|
|
1709
|
+
if [ -n "$cmd" ]; then
|
|
1710
|
+
configured=true; ran=true
|
|
1711
|
+
if ( cd "$repo_root" && bash -c "$cmd" ) >"$log" 2>&1; then ok=true; else ok=false; fi
|
|
1712
|
+
fi
|
|
1713
|
+
local t; t=$(tail -c 1400 "$log" 2>/dev/null || true); rm -f "$log"
|
|
1714
|
+
printf '{"ok":%s,"configured":%s,"ran":%s,"cmd":"%s","log_tail":"%s"}\n' \
|
|
1715
|
+
"$ok" "$configured" "$ran" "$(json_escape "$cmd")" "$(json_escape "$t")"
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
# ─────────────────────────────── cr-setup / cr-archive ──────────────────────
|
|
1719
|
+
# Change-request scaffolding: a delta lives under <feature>/changes/<cr-id>/,
|
|
1720
|
+
# archives to <feature>/changes/archive/<cr-id>/, and bumps the version on close.
|
|
1721
|
+
# Single-active-change guard: cr-setup refuses to open a 2nd concurrent change on
|
|
1722
|
+
# the same aggregate (design must close the open one first) unless --force.
|
|
1723
|
+
cmd_cr_setup() {
|
|
1724
|
+
local cr_id="" feature_dir_arg="" title="" force=""
|
|
1725
|
+
while [ $# -gt 0 ]; do
|
|
1726
|
+
case "$1" in
|
|
1727
|
+
--cr-id) cr_id="$2"; shift 2 ;;
|
|
1728
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
1729
|
+
--title) title="$2"; shift 2 ;;
|
|
1730
|
+
--force) force=1; shift ;;
|
|
1731
|
+
--help|-h) echo "cr-setup --cr-id <id> [--feature-dir <path>] [--title <t>] [--force]"; return 0 ;;
|
|
1732
|
+
*) echo "cr-setup: unknown arg '$1'" >&2; return 1 ;;
|
|
1733
|
+
esac
|
|
1734
|
+
done
|
|
1735
|
+
[ -n "$cr_id" ] || cr_id="CR-$(date +%Y%m%d-%H%M%S)"
|
|
1736
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1737
|
+
local fd="$feature_dir_arg"
|
|
1738
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
1739
|
+
[ -n "$fd" ] || { echo "cr-setup: no feature set and no --feature-dir" >&2; return 1; }
|
|
1740
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
1741
|
+
# Single-active-change guard: open the change FIRST so a second concurrent CR on
|
|
1742
|
+
# the same aggregate is refused BEFORE any changes/<cr>/ dir is scaffolded (no
|
|
1743
|
+
# orphan left behind). Re-opening the same cr_id is a no-op. --force overrides.
|
|
1744
|
+
local av_open
|
|
1745
|
+
if ! av_open=$(_active_version "$fd" open "$cr_id" "$force"); then
|
|
1746
|
+
echo "cr-setup: refusing to open '$cr_id' — a change is already open on this feature; finish it (finalize-delta -> analyze-slice -> close-delta) first, or pass --force." >&2
|
|
1747
|
+
printf '%s\n' "$av_open"
|
|
1748
|
+
return 1
|
|
1749
|
+
fi
|
|
1750
|
+
local cr_dir="$fd/changes/$cr_id"
|
|
1751
|
+
mkdir -p "$cr_dir"
|
|
1752
|
+
local dspec="$cr_dir/delta-spec.md" ddesign="$cr_dir/delta-design.md" tpl created=false
|
|
1753
|
+
if [ ! -f "$dspec" ]; then
|
|
1754
|
+
tpl=$(resolve_template "delta-spec-template" "$repo_root") || true
|
|
1755
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$dspec"; else touch "$dspec"; fi
|
|
1756
|
+
created=true
|
|
1757
|
+
fi
|
|
1758
|
+
if [ ! -f "$ddesign" ]; then
|
|
1759
|
+
tpl=$(resolve_template "delta-design-template" "$repo_root") || true
|
|
1760
|
+
if [ -n "$tpl" ] && [ -f "$tpl" ]; then cp "$tpl" "$ddesign"; else touch "$ddesign"; fi
|
|
1761
|
+
created=true
|
|
1762
|
+
fi
|
|
1763
|
+
printf '{"cr_id":"%s","cr_dir":"%s","delta_spec":"%s","delta_design":"%s","title":"%s","created":%s}\n' \
|
|
1764
|
+
"$(json_escape "$cr_id")" "$(json_escape "$cr_dir")" "$(json_escape "$dspec")" \
|
|
1765
|
+
"$(json_escape "$ddesign")" "$(json_escape "$title")" "$created"
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
cmd_cr_archive() {
|
|
1769
|
+
local cr_id="" feature_dir_arg=""
|
|
1770
|
+
while [ $# -gt 0 ]; do
|
|
1771
|
+
case "$1" in
|
|
1772
|
+
--cr-id) cr_id="$2"; shift 2 ;;
|
|
1773
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
1774
|
+
--help|-h) echo "cr-archive --cr-id <id> [--feature-dir <path>]"; return 0 ;;
|
|
1775
|
+
*) echo "cr-archive: unknown arg '$1'" >&2; return 1 ;;
|
|
1776
|
+
esac
|
|
1777
|
+
done
|
|
1778
|
+
[ -n "$cr_id" ] || { echo "cr-archive: --cr-id required" >&2; return 1; }
|
|
1779
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1780
|
+
local fd="$feature_dir_arg"
|
|
1781
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
1782
|
+
[ -n "$fd" ] || { echo "cr-archive: no feature set and no --feature-dir" >&2; return 1; }
|
|
1783
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
1784
|
+
local cr_dir="$fd/changes/$cr_id" archive_root="$fd/changes/archive" archived=""
|
|
1785
|
+
if [ -d "$cr_dir" ]; then
|
|
1786
|
+
mkdir -p "$archive_root"
|
|
1787
|
+
rm -rf "$archive_root/$cr_id"
|
|
1788
|
+
mv "$cr_dir" "$archive_root/$cr_id"
|
|
1789
|
+
archived="$archive_root/$cr_id"
|
|
1790
|
+
fi
|
|
1791
|
+
# Close the change in active_version.json (single version source): bump minor +
|
|
1792
|
+
# clear active_change. (Supersedes the old bare <feature>/VERSION file.)
|
|
1793
|
+
local av; av=$(_active_version "$fd" close)
|
|
1794
|
+
AW_AV="$av" AW_ARCHIVED="$archived" python3 - <<'PY'
|
|
1795
|
+
import json, os
|
|
1796
|
+
try: av = json.loads(os.environ.get("AW_AV") or "{}")
|
|
1797
|
+
except Exception: av = {}
|
|
1798
|
+
print(json.dumps({"ok": True, "archived_dir": os.environ.get("AW_ARCHIVED", ""),
|
|
1799
|
+
"version": av.get("version", "v1.1"), "previous": av.get("previous", "v1.0"),
|
|
1800
|
+
"closed_change": av.get("closed_change", "")}))
|
|
1801
|
+
PY
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
# Stamp a version into the MASTER documents' `**Version**:` headers — the
|
|
1805
|
+
# deterministic twin of cr-archive's active_version.json bump. close-delta calls
|
|
1806
|
+
# this with the version cr-archive just computed, so all THREE carriers of the
|
|
1807
|
+
# number (spec.md, design.md, active_version.json) move in ONE stage by ONE
|
|
1808
|
+
# mechanism. Before this, design.md's header was bumped by the MODEL at
|
|
1809
|
+
# finalize-delta — a stage early, and guessing the target since the number was
|
|
1810
|
+
# never passed into the merge prompt — while spec.md's header was bumped by
|
|
1811
|
+
# nobody at all and sat at v1.0 forever.
|
|
1812
|
+
# Idempotent: re-stamping the same version reports `unchanged`, writes nothing.
|
|
1813
|
+
# Only the value moves; any trailing inline comment on the header line is kept.
|
|
1814
|
+
cmd_version_stamp() {
|
|
1815
|
+
local version="" feature_dir_arg=""
|
|
1816
|
+
while [ $# -gt 0 ]; do
|
|
1817
|
+
case "$1" in
|
|
1818
|
+
--version) version="$2"; shift 2 ;;
|
|
1819
|
+
--feature-dir) feature_dir_arg="$2"; shift 2 ;;
|
|
1820
|
+
--help|-h) echo "version-stamp --version <vX.Y> [--feature-dir <path>]"; return 0 ;;
|
|
1821
|
+
*) echo "version-stamp: unknown arg '$1'" >&2; return 1 ;;
|
|
1822
|
+
esac
|
|
1823
|
+
done
|
|
1824
|
+
[ -n "$version" ] || { echo "version-stamp: --version required" >&2; return 1; }
|
|
1825
|
+
local repo_root; repo_root=$(get_repo_root)
|
|
1826
|
+
local fd="$feature_dir_arg"
|
|
1827
|
+
[ -n "$fd" ] || fd=$(read_feature_json_feature_directory "$repo_root")
|
|
1828
|
+
[ -n "$fd" ] || { echo "version-stamp: no feature set and no --feature-dir" >&2; return 1; }
|
|
1829
|
+
[[ "$fd" != /* ]] && fd="$repo_root/$fd"
|
|
1830
|
+
AW_FD="$fd" AW_VER="$version" python3 - <<'PY'
|
|
1831
|
+
import json, os, re
|
|
1832
|
+
fd = os.environ["AW_FD"]; ver = os.environ["AW_VER"].strip()
|
|
1833
|
+
stamped, unchanged, missing, no_header = [], [], [], []
|
|
1834
|
+
# First `**Version**: <value>` line only — that is the header. The Change History
|
|
1835
|
+
# table's `| Version |` column head does not match this pattern.
|
|
1836
|
+
pat = re.compile(r'^(\*\*Version\*\*:[ \t]*)(\S+)(.*)$', re.M)
|
|
1837
|
+
for name in ("spec.md", "design.md"):
|
|
1838
|
+
p = os.path.join(fd, name)
|
|
1839
|
+
if not os.path.isfile(p):
|
|
1840
|
+
missing.append(name); continue
|
|
1841
|
+
src = open(p, encoding="utf-8").read()
|
|
1842
|
+
m = pat.search(src)
|
|
1843
|
+
if not m:
|
|
1844
|
+
no_header.append(name); continue
|
|
1845
|
+
if m.group(2) == ver:
|
|
1846
|
+
unchanged.append(name); continue
|
|
1847
|
+
open(p, "w", encoding="utf-8").write(
|
|
1848
|
+
src[:m.start()] + m.group(1) + ver + m.group(3) + src[m.end():])
|
|
1849
|
+
stamped.append(name)
|
|
1850
|
+
print(json.dumps({"ok": True, "version": ver, "stamped": stamped,
|
|
1851
|
+
"unchanged": unchanged, "missing": missing,
|
|
1852
|
+
"no_header": no_header, "count": len(stamped)}))
|
|
1853
|
+
PY
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
case "$sub" in
|
|
1857
|
+
resolve-domain) cmd_resolve_domain "$@" ;;
|
|
1858
|
+
resolve-tech) cmd_resolve_tech "$@" ;;
|
|
1859
|
+
scaffold) cmd_scaffold "$@" ;;
|
|
1860
|
+
resolve-hooks) cmd_resolve_hooks "$@" ;;
|
|
1861
|
+
create-checklist) cmd_create_checklist "$@" ;;
|
|
1862
|
+
list-domains) cmd_list_domains "$@" ;;
|
|
1863
|
+
set-domain) cmd_set_domain "$@" ;;
|
|
1864
|
+
list-features) cmd_list_features "$@" ;;
|
|
1865
|
+
set-feature) cmd_set_feature "$@" ;;
|
|
1866
|
+
version-read) cmd_version_read "$@" ;;
|
|
1867
|
+
version-init) cmd_version_init "$@" ;;
|
|
1868
|
+
feature-paths) cmd_feature_paths "$@" ;;
|
|
1869
|
+
design-scaffold) cmd_design_scaffold "$@" ;;
|
|
1870
|
+
build-compile) cmd_build_compile "$@" ;;
|
|
1871
|
+
test-run) cmd_test_run "$@" ;;
|
|
1872
|
+
run-migrate) cmd_run_migrate "$@" ;;
|
|
1873
|
+
deploy-dispatch) cmd_deploy_dispatch "$@" ;;
|
|
1874
|
+
cr-setup) cmd_cr_setup "$@" ;;
|
|
1875
|
+
cr-archive) cmd_cr_archive "$@" ;;
|
|
1876
|
+
version-stamp) cmd_version_stamp "$@" ;;
|
|
1877
|
+
checklist-read) cmd_checklist_read "$@" ;;
|
|
1878
|
+
checklist-apply) cmd_checklist_apply "$@" ;;
|
|
1879
|
+
detect-project) cmd_detect_project "$@" ;;
|
|
1880
|
+
foundation-list) cmd_foundation_list "$@" ;;
|
|
1881
|
+
foundation-locate) cmd_foundation_locate "$@" ;;
|
|
1882
|
+
foundation-unpack) cmd_foundation_unpack "$@" ;;
|
|
1883
|
+
foundation-verify) cmd_foundation_verify "$@" ;;
|
|
1884
|
+
foundation-stash-dbcontext) cmd_foundation_stash_dbcontext "$@" ;;
|
|
1885
|
+
list-components) cmd_list_components "$@" ;;
|
|
1886
|
+
component-catalog) cmd_component_catalog "$@" ;;
|
|
1887
|
+
component-verify) cmd_component_verify "$@" ;;
|
|
1888
|
+
find-host-project) cmd_find_host_project "$@" ;;
|
|
1889
|
+
setup-constitution) cmd_setup_constitution "$@" ;;
|
|
1890
|
+
compose-constitution) cmd_compose_constitution "$@" ;;
|
|
1891
|
+
constitution-verify) cmd_constitution_verify "$@" ;;
|
|
1892
|
+
agent-context-update) cmd_agent_context_update "$@" ;;
|
|
1893
|
+
""|--help|-h)
|
|
1894
|
+
sed -n '2,34p' "$0" | sed 's/^#\{0,1\} \{0,1\}//' ;;
|
|
1895
|
+
*)
|
|
1896
|
+
echo "Unknown subcommand: $sub" >&2
|
|
1897
|
+
echo "Run '$0 --help' for usage." >&2
|
|
1898
|
+
exit 1 ;;
|
|
1899
|
+
esac
|