@jaimevalasek/aioson 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +153 -10
- package/docs/en/cli-reference.md +56 -1
- package/docs/en/i18n.md +18 -18
- package/docs/en/schemas/index.json +10 -0
- package/docs/en/schemas/parallel-assign.schema.json +9 -0
- package/docs/en/schemas/parallel-doctor.schema.json +36 -0
- package/docs/en/schemas/parallel-guard.schema.json +63 -0
- package/docs/en/schemas/parallel-merge.schema.json +84 -0
- package/docs/en/schemas/parallel-status.schema.json +91 -1
- package/docs/integrations/apps-publish-marketplace.md +94 -0
- package/docs/pt/README.md +9 -0
- package/docs/pt/agentes.md +324 -3
- package/docs/pt/clientes-ai.md +7 -3
- package/docs/pt/comandos-cli.md +160 -13
- package/docs/pt/compress-agents.md +304 -0
- package/docs/pt/design-docs-governance.md +59 -0
- package/docs/pt/feature-archive.md +191 -0
- package/docs/pt/genome-3.0-spec.md +115 -4
- package/docs/pt/genome-distribution.md +232 -0
- package/docs/pt/inicio-rapido.md +1 -0
- package/docs/pt/motor-hardening.md +492 -0
- package/docs/pt/runner-system.md +113 -0
- package/package.json +2 -1
- package/src/agent-manifests.js +66 -0
- package/src/agents.js +27 -7
- package/src/autonomy-policy.js +139 -0
- package/src/brain-query.js +161 -0
- package/src/cli.js +1377 -1099
- package/src/commands/agents.js +102 -7
- package/src/commands/artifact-validate.js +33 -4
- package/src/commands/auth.js +272 -0
- package/src/commands/brain-query.js +44 -0
- package/src/commands/briefing.js +344 -0
- package/src/commands/commit-prepare.js +547 -0
- package/src/commands/compress-agents.js +416 -0
- package/src/commands/context-health.js +4 -2
- package/src/commands/context-trim.js +17 -11
- package/src/commands/design-hybrid-options.js +3 -3
- package/src/commands/devlog-process.js +6 -4
- package/src/commands/dossier.js +423 -0
- package/src/commands/feature-archive.js +513 -0
- package/src/commands/feature-close.js +123 -18
- package/src/commands/gate-approve.js +198 -0
- package/src/commands/gate-check.js +24 -5
- package/src/commands/genome-doctor.js +166 -9
- package/src/commands/git-guard.js +170 -0
- package/src/commands/harness.js +121 -0
- package/src/commands/implementation-plan.js +47 -20
- package/src/commands/init.js +6 -2
- package/src/commands/install.js +6 -2
- package/src/commands/live.js +497 -56
- package/src/commands/locale-apply.js +9 -6
- package/src/commands/locale-diff.js +11 -112
- package/src/commands/mcp-doctor.js +2 -1
- package/src/commands/mcp-init.js +4 -10
- package/src/commands/memory.js +234 -0
- package/src/commands/parallel-assign.js +107 -27
- package/src/commands/parallel-doctor.js +416 -3
- package/src/commands/parallel-guard.js +241 -0
- package/src/commands/parallel-init.js +66 -4
- package/src/commands/parallel-merge.js +299 -0
- package/src/commands/parallel-status.js +147 -3
- package/src/commands/preflight.js +63 -4
- package/src/commands/qa-init.js +10 -5
- package/src/commands/revision.js +235 -0
- package/src/commands/scaffold-complete.js +188 -0
- package/src/commands/security-audit.js +275 -0
- package/src/commands/security-scan.js +376 -0
- package/src/commands/self-implement-loop.js +46 -2
- package/src/commands/setup-context.js +11 -10
- package/src/commands/squad-agent-create.js +51 -9
- package/src/commands/squad-investigate.js +53 -0
- package/src/commands/squad-plan.js +33 -1
- package/src/commands/squad-scaffold.js +4 -3
- package/src/commands/squad-score.js +71 -14
- package/src/commands/squad-status.js +22 -1
- package/src/commands/squad-validate.js +93 -2
- package/src/commands/store-genome.js +304 -0
- package/src/commands/store-skill.js +247 -0
- package/src/commands/store-squad.js +431 -0
- package/src/commands/store-system.js +392 -0
- package/src/commands/tool-capabilities.js +63 -0
- package/src/commands/update.js +3 -3
- package/src/commands/verify-gate.js +40 -0
- package/src/commands/workflow-execute.js +644 -155
- package/src/commands/workflow-harden.js +231 -0
- package/src/commands/workflow-heal.js +136 -0
- package/src/commands/workflow-next.js +460 -22
- package/src/commands/workflow-status.js +328 -138
- package/src/commands/workspace.js +144 -0
- package/src/constants.js +55 -75
- package/src/context-memory.js +133 -4
- package/src/context-writer.js +2 -1
- package/src/context.js +32 -2
- package/src/doctor.js +46 -6
- package/src/dossier/codemap-store.js +267 -0
- package/src/dossier/dossier-bootstrap.js +222 -0
- package/src/dossier/dossier-compact.js +159 -0
- package/src/dossier/lock.js +128 -0
- package/src/dossier/revision-store.js +313 -0
- package/src/dossier/schema.js +155 -0
- package/src/dossier/store.js +400 -0
- package/src/execution-gateway.js +3 -0
- package/src/friction-scanner.js +202 -0
- package/src/genome-schema.js +24 -1
- package/src/genomes.js +33 -0
- package/src/handoff-contract.js +363 -0
- package/src/handoff-validator.js +45 -0
- package/src/harness/circuit-breaker.js +135 -0
- package/src/i18n/messages/en.js +317 -22
- package/src/i18n/messages/es.js +259 -18
- package/src/i18n/messages/fr.js +260 -18
- package/src/i18n/messages/pt-BR.js +313 -22
- package/src/install-profile.js +0 -16
- package/src/installer.js +70 -6
- package/src/lib/git-commit-guard.js +691 -0
- package/src/lib/security/artifact-reader.js +167 -0
- package/src/lib/security/exit-codes.js +51 -0
- package/src/lib/security/findings-writer.js +176 -0
- package/src/lib/security/runtime-events.js +77 -0
- package/src/lib/security/secrets-regex.js +115 -0
- package/src/lib/store/security-scan.js +173 -0
- package/src/lib/terminal-checkbox.js +130 -0
- package/src/lib/tmux-launcher.js +163 -0
- package/src/lib/tool-capabilities.js +102 -0
- package/src/locales.js +12 -8
- package/src/parallel-workspace.js +756 -0
- package/src/parser.js +8 -1
- package/src/path-guard.js +47 -0
- package/src/preflight-engine.js +237 -26
- package/src/self-healing.js +142 -0
- package/src/session-handoff.js +111 -1
- package/src/squad/squad-scaffold.js +183 -19
- package/src/test-briefing.js +226 -0
- package/src/updater.js +1 -1
- package/src/utils.js +3 -0
- package/src/workflow-gates.js +185 -0
- package/template/.aioson/agents/analyst.md +76 -130
- package/template/.aioson/agents/architect.md +53 -86
- package/template/.aioson/agents/committer.md +161 -0
- package/template/.aioson/agents/copywriter.md +463 -0
- package/template/.aioson/agents/cypher.md +252 -0
- package/template/.aioson/agents/dev.md +112 -600
- package/template/.aioson/agents/deyvin.md +33 -235
- package/template/.aioson/agents/discover.md +235 -0
- package/template/.aioson/agents/discovery-design-doc.md +17 -252
- package/template/.aioson/agents/genome.md +76 -26
- package/template/.aioson/agents/manifests/analyst.manifest.json +26 -0
- package/template/.aioson/agents/manifests/architect.manifest.json +23 -0
- package/template/.aioson/agents/manifests/committer.manifest.json +23 -0
- package/template/.aioson/agents/manifests/dev.manifest.json +37 -0
- package/template/.aioson/agents/manifests/orchestrator.manifest.json +30 -0
- package/template/.aioson/agents/manifests/pentester.manifest.json +39 -0
- package/template/.aioson/agents/manifests/pm.manifest.json +26 -0
- package/template/.aioson/agents/manifests/product.manifest.json +23 -0
- package/template/.aioson/agents/manifests/qa.manifest.json +25 -0
- package/template/.aioson/agents/manifests/setup.manifest.json +20 -0
- package/template/.aioson/agents/manifests/ux-ui.manifest.json +24 -0
- package/template/.aioson/agents/neo.md +10 -8
- package/template/.aioson/agents/orache.md +2 -6
- package/template/.aioson/agents/orchestrator.md +81 -182
- package/template/.aioson/agents/pentester.md +235 -0
- package/template/.aioson/agents/pm.md +40 -104
- package/template/.aioson/agents/product.md +99 -344
- package/template/.aioson/agents/profiler-enricher.md +57 -6
- package/template/.aioson/agents/profiler-forge.md +17 -7
- package/template/.aioson/agents/profiler-researcher.md +29 -6
- package/template/.aioson/agents/qa.md +165 -410
- package/template/.aioson/agents/setup.md +52 -262
- package/template/.aioson/agents/sheldon.md +122 -754
- package/template/.aioson/agents/site-forge.md +111 -1583
- package/template/.aioson/agents/squad.md +139 -1820
- package/template/.aioson/agents/tester.md +10 -0
- package/template/.aioson/agents/ux-ui.md +103 -645
- package/template/.aioson/agents/validator.md +69 -0
- package/template/.aioson/brains/scripts/query.js +5 -1
- package/template/.aioson/config/autonomy-protocol.json +43 -0
- package/template/.aioson/config.md +43 -15
- package/template/.aioson/constitution.md +36 -33
- package/template/.aioson/context/design-doc.md +136 -0
- package/template/.aioson/context/project-map.md +57 -0
- package/template/.aioson/design-docs/code-reuse.md +48 -0
- package/template/.aioson/design-docs/componentization.md +47 -0
- package/template/.aioson/design-docs/file-size.md +52 -0
- package/template/.aioson/design-docs/folder-structure.md +51 -0
- package/template/.aioson/design-docs/naming.md +54 -0
- package/template/.aioson/docs/LAYERS.md +12 -2
- package/template/.aioson/docs/dev/execution-discipline.md +106 -0
- package/template/.aioson/docs/dev/stack-conventions.md +83 -0
- package/template/.aioson/docs/deyvin/continuity-recovery.md +57 -0
- package/template/.aioson/docs/deyvin/debugging-escalation.md +30 -0
- package/template/.aioson/docs/deyvin/pair-execution.md +44 -0
- package/template/.aioson/docs/deyvin/runtime-handoffs.md +36 -0
- package/template/.aioson/docs/product/conversation-playbook.md +116 -0
- package/template/.aioson/docs/product/prd-contract.md +107 -0
- package/template/.aioson/docs/product/quality-lens.md +57 -0
- package/template/.aioson/docs/product/research-loop.md +65 -0
- package/template/.aioson/docs/sheldon/enrichment-paths.md +134 -0
- package/template/.aioson/docs/sheldon/quality-lens.md +57 -0
- package/template/.aioson/docs/sheldon/research-loop.md +56 -0
- package/template/.aioson/docs/sheldon/web-intelligence.md +75 -0
- package/template/.aioson/docs/site-forge-build.md +195 -0
- package/template/.aioson/docs/site-forge-extraction.md +135 -0
- package/template/.aioson/docs/site-forge-qa.md +155 -0
- package/template/.aioson/docs/site-forge-recon.md +434 -0
- package/template/.aioson/docs/site-forge-transform.md +249 -0
- package/template/.aioson/docs/squad/content-output.md +91 -0
- package/template/.aioson/docs/squad/creation-flow.md +135 -0
- package/template/.aioson/docs/squad/domain-classification.md +117 -0
- package/template/.aioson/docs/squad/genome-bindings.md +47 -0
- package/template/.aioson/docs/squad/package-contract.md +234 -0
- package/template/.aioson/docs/squad/quality-lens.md +56 -0
- package/template/.aioson/docs/squad/research-loop.md +59 -0
- package/template/.aioson/docs/squad/session-operations.md +117 -0
- package/template/.aioson/docs/squad/workflow-quality.md +165 -0
- package/template/.aioson/docs/ux-ui/accessibility-audit.md +55 -0
- package/template/.aioson/docs/ux-ui/audit-mode.md +86 -0
- package/template/.aioson/docs/ux-ui/component-map.md +35 -0
- package/template/.aioson/docs/ux-ui/design-execution.md +111 -0
- package/template/.aioson/docs/ux-ui/design-gate.md +27 -0
- package/template/.aioson/docs/ux-ui/research-mode.md +39 -0
- package/template/.aioson/docs/ux-ui/site-delivery.md +156 -0
- package/template/.aioson/docs/ux-ui/token-contract.md +57 -0
- package/template/.aioson/genomes/copywriting.md +204 -0
- package/template/.aioson/genomes/copywriting.meta.json +48 -0
- package/template/.aioson/git-guard.json +11 -0
- package/template/.aioson/mcp/servers.md +0 -1
- package/template/.aioson/rules/agent-language-policy.md +93 -0
- package/template/.aioson/rules/aioson-context-boundary.md +63 -0
- package/template/.aioson/rules/canonical-path-contract.md +47 -0
- package/template/.aioson/rules/data-format-convention.md +24 -86
- package/template/.aioson/rules/disk-first-artifacts.md +44 -0
- package/template/.aioson/rules/output-brevity.md +44 -0
- package/template/.aioson/rules/prd-section-ownership.md +49 -0
- package/template/.aioson/rules/security-baseline.md +139 -0
- package/template/.aioson/rules/spec-level-ownership.md +61 -0
- package/template/.aioson/rules/squad-driver-pattern.md +81 -0
- package/template/.aioson/schemas/squad-blueprint.schema.json +24 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +44 -0
- package/template/.aioson/skills/design/cognitive-core-ui/references/motion.md +2 -0
- package/template/.aioson/skills/marketing/references/anti-patterns.md +254 -0
- package/template/.aioson/skills/marketing/references/fascinations.md +192 -0
- package/template/.aioson/skills/marketing/references/five-acts.md +248 -0
- package/template/.aioson/skills/marketing/references/market-intelligence.md +198 -0
- package/template/.aioson/skills/marketing/references/offer-structure.md +203 -0
- package/template/.aioson/skills/marketing/references/one-belief.md +149 -0
- package/template/.aioson/skills/marketing/references/patterns.md +218 -0
- package/template/.aioson/skills/marketing/references/pms-research.md +193 -0
- package/template/.aioson/skills/marketing/vsl-craft.md +385 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +30 -0
- package/template/.aioson/skills/process/secure-tdd/SKILL.md +97 -0
- package/template/.aioson/skills/process/secure-tdd/references/nextjs.md +81 -0
- package/template/.aioson/skills/process/secure-tdd/references/node-express.md +91 -0
- package/template/.aioson/skills/process/secure-tdd/references/planned-stacks.md +33 -0
- package/template/.aioson/skills/static/harness-validate/SKILL.md +46 -0
- package/template/.aioson/skills/static/landing-page-deploy.md +192 -0
- package/template/.aioson/skills/static/landing-page-forge.md +730 -0
- package/template/.aioson/skills/static/ui-ux-modern.md +1 -0
- package/template/.aioson/skills/static/web-research-cache.md +3 -0
- package/template/.aioson/tasks/squad-create.md +56 -7
- package/template/.aioson/tasks/squad-design.md +80 -2
- package/template/.aioson/tasks/squad-investigate.md +14 -1
- package/template/.aioson/templates/squads/digital-marketing-agency/template.json +96 -0
- package/template/.claude/commands/aioson/agent/committer.md +5 -0
- package/template/.claude/commands/aioson/agent/copywriter.md +5 -0
- package/template/.claude/commands/aioson/agent/cypher.md +5 -0
- package/template/.claude/commands/aioson/agent/pair.md +5 -0
- package/template/.claude/commands/aioson/agent/validator.md +5 -0
- package/template/.gemini/commands/aios-analyst.toml +6 -3
- package/template/.gemini/commands/aios-architect.toml +7 -6
- package/template/.gemini/commands/aios-committer.toml +7 -0
- package/template/.gemini/commands/aios-copywriter.toml +7 -0
- package/template/.gemini/commands/aios-cypher.toml +7 -0
- package/template/.gemini/commands/aios-dev.toml +8 -7
- package/template/.gemini/commands/aios-deyvin.toml +6 -5
- package/template/.gemini/commands/aios-discovery-design-doc.toml +6 -3
- package/template/.gemini/commands/aios-genome.toml +7 -0
- package/template/.gemini/commands/aios-neo.toml +5 -3
- package/template/.gemini/commands/aios-orache.toml +7 -0
- package/template/.gemini/commands/aios-orchestrator.toml +8 -7
- package/template/.gemini/commands/aios-pair.toml +6 -5
- package/template/.gemini/commands/aios-pm.toml +8 -7
- package/template/.gemini/commands/aios-product.toml +5 -3
- package/template/.gemini/commands/aios-qa.toml +6 -5
- package/template/.gemini/commands/aios-setup.toml +5 -2
- package/template/.gemini/commands/aios-sheldon.toml +7 -0
- package/template/.gemini/commands/aios-site-forge.toml +7 -0
- package/template/.gemini/commands/aios-squad.toml +7 -0
- package/template/.gemini/commands/aios-tester.toml +6 -5
- package/template/.gemini/commands/aios-ux-ui.toml +8 -7
- package/template/.gemini/commands/aios-validator.toml +7 -0
- package/template/AGENTS.md +12 -1
- package/template/CLAUDE.md +6 -1
- package/template/.aioson/locales/en/agents/analyst.md +0 -244
- package/template/.aioson/locales/en/agents/architect.md +0 -245
- package/template/.aioson/locales/en/agents/dev.md +0 -397
- package/template/.aioson/locales/en/agents/deyvin.md +0 -137
- package/template/.aioson/locales/en/agents/discovery-design-doc.md +0 -27
- package/template/.aioson/locales/en/agents/genome.md +0 -212
- package/template/.aioson/locales/en/agents/neo.md +0 -8
- package/template/.aioson/locales/en/agents/orache.md +0 -6
- package/template/.aioson/locales/en/agents/orchestrator.md +0 -189
- package/template/.aioson/locales/en/agents/pair.md +0 -5
- package/template/.aioson/locales/en/agents/pm.md +0 -84
- package/template/.aioson/locales/en/agents/product.md +0 -378
- package/template/.aioson/locales/en/agents/profiler-enricher.md +0 -5
- package/template/.aioson/locales/en/agents/profiler-forge.md +0 -5
- package/template/.aioson/locales/en/agents/profiler-researcher.md +0 -5
- package/template/.aioson/locales/en/agents/qa.md +0 -270
- package/template/.aioson/locales/en/agents/setup.md +0 -421
- package/template/.aioson/locales/en/agents/sheldon.md +0 -455
- package/template/.aioson/locales/en/agents/squad.md +0 -449
- package/template/.aioson/locales/en/agents/tester.md +0 -6
- package/template/.aioson/locales/en/agents/ux-ui.md +0 -668
- package/template/.aioson/locales/es/agents/analyst.md +0 -225
- package/template/.aioson/locales/es/agents/architect.md +0 -245
- package/template/.aioson/locales/es/agents/dev.md +0 -370
- package/template/.aioson/locales/es/agents/deyvin.md +0 -99
- package/template/.aioson/locales/es/agents/discovery-design-doc.md +0 -21
- package/template/.aioson/locales/es/agents/genome.md +0 -104
- package/template/.aioson/locales/es/agents/neo.md +0 -50
- package/template/.aioson/locales/es/agents/orache.md +0 -105
- package/template/.aioson/locales/es/agents/orchestrator.md +0 -194
- package/template/.aioson/locales/es/agents/pair.md +0 -7
- package/template/.aioson/locales/es/agents/pm.md +0 -90
- package/template/.aioson/locales/es/agents/product.md +0 -372
- package/template/.aioson/locales/es/agents/profiler-enricher.md +0 -7
- package/template/.aioson/locales/es/agents/profiler-forge.md +0 -7
- package/template/.aioson/locales/es/agents/profiler-researcher.md +0 -7
- package/template/.aioson/locales/es/agents/qa.md +0 -198
- package/template/.aioson/locales/es/agents/setup.md +0 -405
- package/template/.aioson/locales/es/agents/sheldon.md +0 -309
- package/template/.aioson/locales/es/agents/squad.md +0 -532
- package/template/.aioson/locales/es/agents/tester.md +0 -9
- package/template/.aioson/locales/es/agents/ux-ui.md +0 -212
- package/template/.aioson/locales/fr/agents/analyst.md +0 -225
- package/template/.aioson/locales/fr/agents/architect.md +0 -245
- package/template/.aioson/locales/fr/agents/dev.md +0 -370
- package/template/.aioson/locales/fr/agents/deyvin.md +0 -99
- package/template/.aioson/locales/fr/agents/discovery-design-doc.md +0 -21
- package/template/.aioson/locales/fr/agents/genome.md +0 -104
- package/template/.aioson/locales/fr/agents/neo.md +0 -50
- package/template/.aioson/locales/fr/agents/orache.md +0 -106
- package/template/.aioson/locales/fr/agents/orchestrator.md +0 -194
- package/template/.aioson/locales/fr/agents/pair.md +0 -7
- package/template/.aioson/locales/fr/agents/pm.md +0 -90
- package/template/.aioson/locales/fr/agents/product.md +0 -372
- package/template/.aioson/locales/fr/agents/profiler-enricher.md +0 -7
- package/template/.aioson/locales/fr/agents/profiler-forge.md +0 -7
- package/template/.aioson/locales/fr/agents/profiler-researcher.md +0 -7
- package/template/.aioson/locales/fr/agents/qa.md +0 -198
- package/template/.aioson/locales/fr/agents/setup.md +0 -405
- package/template/.aioson/locales/fr/agents/sheldon.md +0 -309
- package/template/.aioson/locales/fr/agents/squad.md +0 -532
- package/template/.aioson/locales/fr/agents/tester.md +0 -9
- package/template/.aioson/locales/fr/agents/ux-ui.md +0 -212
- package/template/.aioson/locales/pt-BR/agents/analyst.md +0 -319
- package/template/.aioson/locales/pt-BR/agents/architect.md +0 -284
- package/template/.aioson/locales/pt-BR/agents/dev.md +0 -483
- package/template/.aioson/locales/pt-BR/agents/deyvin.md +0 -184
- package/template/.aioson/locales/pt-BR/agents/discovery-design-doc.md +0 -198
- package/template/.aioson/locales/pt-BR/agents/genome.md +0 -297
- package/template/.aioson/locales/pt-BR/agents/neo.md +0 -208
- package/template/.aioson/locales/pt-BR/agents/orache.md +0 -137
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +0 -324
- package/template/.aioson/locales/pt-BR/agents/pair.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/pm.md +0 -182
- package/template/.aioson/locales/pt-BR/agents/product.md +0 -466
- package/template/.aioson/locales/pt-BR/agents/profiler-enricher.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/profiler-forge.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/profiler-researcher.md +0 -5
- package/template/.aioson/locales/pt-BR/agents/qa.md +0 -300
- package/template/.aioson/locales/pt-BR/agents/setup.md +0 -533
- package/template/.aioson/locales/pt-BR/agents/sheldon.md +0 -323
- package/template/.aioson/locales/pt-BR/agents/squad.md +0 -1330
- package/template/.aioson/locales/pt-BR/agents/tester.md +0 -449
- package/template/.aioson/locales/pt-BR/agents/ux-ui.md +0 -669
- package/template/.aioson/skills/design-system/components/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/dashboards/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/foundations/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/motion/SKILL.md:Zone.Identifier +0 -0
- package/template/.aioson/skills/design-system/patterns/SKILL.md:Zone.Identifier +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson feature:archive — move artefatos de uma feature done para .aioson/context/done/{slug}/
|
|
5
|
+
*
|
|
6
|
+
* Designed to be called by agents automatically (e.g. from feature:close --verdict=PASS)
|
|
7
|
+
* so the end user never needs to type archive commands manually.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* aioson feature:archive . --feature=checkout
|
|
11
|
+
* aioson feature:archive . --feature=checkout --dry-run
|
|
12
|
+
* aioson feature:archive . --feature=checkout --restore
|
|
13
|
+
* aioson feature:archive . --feature=checkout --json
|
|
14
|
+
* aioson feature:archive . --feature=checkout --force (skip features.md status guard)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs/promises');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { contextDir, readFileSafe } = require('../preflight-engine');
|
|
20
|
+
|
|
21
|
+
const ARCHIVED_EXTENSIONS = ['md', 'yaml', 'yml', 'json'];
|
|
22
|
+
|
|
23
|
+
const GLOBAL_FILES = new Set([
|
|
24
|
+
'project.context.md',
|
|
25
|
+
'project-pulse.md',
|
|
26
|
+
'project-map.md',
|
|
27
|
+
'context-pack.md',
|
|
28
|
+
'memory-index.md',
|
|
29
|
+
'module-src.md',
|
|
30
|
+
'features.md',
|
|
31
|
+
'dev-state.md',
|
|
32
|
+
'tasks.md',
|
|
33
|
+
'discovery.md',
|
|
34
|
+
'design-doc.md',
|
|
35
|
+
'prd.md',
|
|
36
|
+
'architecture.md',
|
|
37
|
+
'spec.md',
|
|
38
|
+
'spec.md.template',
|
|
39
|
+
'test-plan.md',
|
|
40
|
+
'test-inventory.md',
|
|
41
|
+
'handoff-protocol.json',
|
|
42
|
+
'last-handoff.json',
|
|
43
|
+
'hardening-report.md',
|
|
44
|
+
'qa-report-test-coverage.md',
|
|
45
|
+
'sheldon-enrichment.md',
|
|
46
|
+
'sheldon-validation.md'
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function escapeRegExp(str) {
|
|
50
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildSlugMatcher(slug) {
|
|
54
|
+
const extsGroup = ARCHIVED_EXTENSIONS.join('|');
|
|
55
|
+
// Accepts `<prefix>-<slug>.<ext>` and also `<prefix>-<slug>-<tail>.<ext>`
|
|
56
|
+
// (e.g. qa-report-pentester-agent-hardening.md). Prefix collisions with other
|
|
57
|
+
// slugs are filtered out via readOtherSlugs() before the matcher is applied.
|
|
58
|
+
return new RegExp(`^[a-z][a-z0-9-]*-${escapeRegExp(slug)}(?:-[a-z0-9][a-z0-9-]*)?\\.(${extsGroup})$`, 'i');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readOtherSlugs(featuresPath, currentSlug) {
|
|
62
|
+
const content = await readFileSafe(featuresPath);
|
|
63
|
+
if (!content) return [];
|
|
64
|
+
const slugs = new Set();
|
|
65
|
+
const lines = content.split(/\r?\n/);
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const m = line.match(/^\|\s*([a-z][a-z0-9-]*)\s*\|/i);
|
|
68
|
+
if (!m) continue;
|
|
69
|
+
const s = m[1].toLowerCase();
|
|
70
|
+
if (s === 'slug' || s === currentSlug.toLowerCase()) continue;
|
|
71
|
+
slugs.add(s);
|
|
72
|
+
}
|
|
73
|
+
return Array.from(slugs);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function belongsToOtherSlug(fileName, slug, otherSlugs) {
|
|
77
|
+
// If another registered slug starts with `${slug}-` and the file suffix
|
|
78
|
+
// matches that longer slug (possibly with an extra tail), the file belongs
|
|
79
|
+
// to the longer-named feature, not to `slug`.
|
|
80
|
+
const base = fileName.replace(/\.(md|yaml|yml|json)$/i, '');
|
|
81
|
+
const slugLower = slug.toLowerCase();
|
|
82
|
+
for (const other of otherSlugs) {
|
|
83
|
+
if (!other.startsWith(`${slugLower}-`)) continue;
|
|
84
|
+
const idx = base.toLowerCase().lastIndexOf(`-${other}`);
|
|
85
|
+
if (idx === -1) continue;
|
|
86
|
+
const afterMatch = base.slice(idx + 1 + other.length);
|
|
87
|
+
if (afterMatch === '' || afterMatch.startsWith('-')) return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function dirExists(dirPath) {
|
|
93
|
+
try {
|
|
94
|
+
const stat = await fs.stat(dirPath);
|
|
95
|
+
return stat.isDirectory();
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readDirSafe(dirPath) {
|
|
102
|
+
try {
|
|
103
|
+
return await fs.readdir(dirPath, { withFileTypes: true });
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function featureStatus(featuresPath, slug) {
|
|
110
|
+
const content = await readFileSafe(featuresPath);
|
|
111
|
+
if (!content) return { exists: false, status: null };
|
|
112
|
+
const row = new RegExp(`\\|\\s*${escapeRegExp(slug)}\\s*\\|\\s*([a-z_]+)\\s*\\|`, 'i');
|
|
113
|
+
const match = content.match(row);
|
|
114
|
+
if (!match) return { exists: false, status: null };
|
|
115
|
+
return { exists: true, status: match[1].toLowerCase() };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function findSlugFiles(ctxDir, slug, otherSlugs = []) {
|
|
119
|
+
const matcher = buildSlugMatcher(slug);
|
|
120
|
+
const entries = await readDirSafe(ctxDir);
|
|
121
|
+
return entries
|
|
122
|
+
.filter((e) => e.isFile())
|
|
123
|
+
.map((e) => e.name)
|
|
124
|
+
.filter((name) => !GLOBAL_FILES.has(name))
|
|
125
|
+
.filter((name) => matcher.test(name))
|
|
126
|
+
.filter((name) => !belongsToOtherSlug(name, slug, otherSlugs));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function findArchivedFiles(archiveDir) {
|
|
130
|
+
const entries = await readDirSafe(archiveDir);
|
|
131
|
+
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function extractSummary(prdPath) {
|
|
135
|
+
const content = await readFileSafe(prdPath);
|
|
136
|
+
if (!content) return null;
|
|
137
|
+
const visionIdx = content.indexOf('## Vision');
|
|
138
|
+
if (visionIdx === -1) return null;
|
|
139
|
+
const after = content.slice(visionIdx + '## Vision'.length);
|
|
140
|
+
const lines = after.split(/\r?\n/);
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const trimmed = line.trim();
|
|
143
|
+
if (!trimmed) continue;
|
|
144
|
+
if (trimmed.startsWith('#')) break;
|
|
145
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) continue;
|
|
146
|
+
return trimmed.replace(/\s+/g, ' ').slice(0, 160);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readCompletedDate(featuresPath, slug) {
|
|
152
|
+
const content = await readFileSafe(featuresPath);
|
|
153
|
+
if (!content) return null;
|
|
154
|
+
const re = new RegExp(`\\|\\s*${escapeRegExp(slug)}\\s*\\|[^|]*\\|[^|]*\\|\\s*([^|]+?)\\s*\\|`, 'i');
|
|
155
|
+
const match = content.match(re);
|
|
156
|
+
if (!match) return null;
|
|
157
|
+
const raw = match[1].trim();
|
|
158
|
+
if (!raw || raw === '—' || raw === '-' || raw.toLowerCase() === 'tbd') return null;
|
|
159
|
+
const isoMatch = raw.match(/\d{4}-\d{2}-\d{2}/);
|
|
160
|
+
return isoMatch ? isoMatch[0] : raw;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function manifestHeader() {
|
|
164
|
+
return [
|
|
165
|
+
'# Archived Features Manifest',
|
|
166
|
+
'',
|
|
167
|
+
'> Features whose artefacts were moved into `.aioson/context/done/{slug}/` after QA sign-off.',
|
|
168
|
+
'> Agents that need historical awareness (@cypher, @neo, @discover, @sheldon) read this file instead of globbing archived PRDs.',
|
|
169
|
+
'',
|
|
170
|
+
'| slug | completed | files | summary |',
|
|
171
|
+
'|------|-----------|-------|---------|',
|
|
172
|
+
''
|
|
173
|
+
].join('\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseManifest(content) {
|
|
177
|
+
if (!content) return { header: manifestHeader(), rows: new Map() };
|
|
178
|
+
const rows = new Map();
|
|
179
|
+
const lines = content.split(/\r?\n/);
|
|
180
|
+
for (const line of lines) {
|
|
181
|
+
const trimmed = line.trim();
|
|
182
|
+
if (!trimmed.startsWith('|')) continue;
|
|
183
|
+
if (/^\|\s*-+\s*\|/.test(trimmed)) continue;
|
|
184
|
+
if (/^\|\s*slug\s*\|/i.test(trimmed)) continue;
|
|
185
|
+
const cols = trimmed.split('|').slice(1, -1).map((c) => c.trim());
|
|
186
|
+
if (cols.length < 4) continue;
|
|
187
|
+
const [slug, completed, files, summary] = cols;
|
|
188
|
+
if (!slug) continue;
|
|
189
|
+
rows.set(slug, { slug, completed, files, summary });
|
|
190
|
+
}
|
|
191
|
+
return { header: manifestHeader(), rows };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function renderManifest(rows) {
|
|
195
|
+
const sorted = Array.from(rows.values()).sort((a, b) => {
|
|
196
|
+
if (a.completed && b.completed) return b.completed.localeCompare(a.completed);
|
|
197
|
+
if (a.completed) return -1;
|
|
198
|
+
if (b.completed) return 1;
|
|
199
|
+
return a.slug.localeCompare(b.slug);
|
|
200
|
+
});
|
|
201
|
+
const body = sorted
|
|
202
|
+
.map((r) => `| ${r.slug} | ${r.completed || '—'} | ${r.files} | ${r.summary || '—'} |`)
|
|
203
|
+
.join('\n');
|
|
204
|
+
return manifestHeader() + body + (body ? '\n' : '');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function updateManifest(manifestPath, entry, mode) {
|
|
208
|
+
const existing = await readFileSafe(manifestPath);
|
|
209
|
+
const { rows } = parseManifest(existing);
|
|
210
|
+
if (mode === 'remove') {
|
|
211
|
+
rows.delete(entry.slug);
|
|
212
|
+
} else {
|
|
213
|
+
rows.set(entry.slug, entry);
|
|
214
|
+
}
|
|
215
|
+
await fs.writeFile(manifestPath, renderManifest(rows), 'utf8');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function runFeatureArchive({ args = [], options = {}, logger }) {
|
|
219
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
220
|
+
const slug = options.feature ? String(options.feature) : null;
|
|
221
|
+
const dryRun = Boolean(options['dry-run'] || options.dryRun);
|
|
222
|
+
const restore = Boolean(options.restore);
|
|
223
|
+
const force = Boolean(options.force);
|
|
224
|
+
const jsonOut = Boolean(options.json);
|
|
225
|
+
|
|
226
|
+
const log = (msg) => { if (logger && !jsonOut) logger.log(msg); };
|
|
227
|
+
|
|
228
|
+
if (!slug) {
|
|
229
|
+
if (jsonOut) return { ok: false, reason: 'missing_feature' };
|
|
230
|
+
log('--feature=<slug> is required.');
|
|
231
|
+
return { ok: false };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(slug)) {
|
|
235
|
+
if (jsonOut) return { ok: false, reason: 'invalid_slug' };
|
|
236
|
+
log(`Invalid slug "${slug}" — use lowercase letters, digits and hyphens only.`);
|
|
237
|
+
return { ok: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ctxDir = contextDir(targetDir);
|
|
241
|
+
const doneDir = path.join(ctxDir, 'done');
|
|
242
|
+
const archiveDir = path.join(doneDir, slug);
|
|
243
|
+
const manifestPath = path.join(doneDir, 'MANIFEST.md');
|
|
244
|
+
const featuresPath = path.join(ctxDir, 'features.md');
|
|
245
|
+
|
|
246
|
+
if (!(await dirExists(ctxDir))) {
|
|
247
|
+
if (jsonOut) return { ok: false, reason: 'no_context_dir' };
|
|
248
|
+
log(`.aioson/context/ not found at ${targetDir}. Run aioson setup first.`);
|
|
249
|
+
return { ok: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (restore) {
|
|
253
|
+
return await runRestore({
|
|
254
|
+
slug, ctxDir, archiveDir, manifestPath, dryRun, jsonOut, log
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const status = await featureStatus(featuresPath, slug);
|
|
259
|
+
if (!status.exists && !force) {
|
|
260
|
+
if (jsonOut) return { ok: false, reason: 'not_in_features', slug };
|
|
261
|
+
log(`Feature "${slug}" is not registered in features.md. Use --force to archive anyway.`);
|
|
262
|
+
return { ok: false };
|
|
263
|
+
}
|
|
264
|
+
if (status.exists && status.status !== 'done' && !force) {
|
|
265
|
+
if (jsonOut) return { ok: false, reason: 'not_done', slug, status: status.status };
|
|
266
|
+
log(`Feature "${slug}" has status "${status.status}" in features.md — only "done" features can be archived. Use --force to override.`);
|
|
267
|
+
return { ok: false };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const otherSlugs = await readOtherSlugs(featuresPath, slug);
|
|
271
|
+
const rootFiles = await findSlugFiles(ctxDir, slug, otherSlugs);
|
|
272
|
+
const alreadyArchived = (await dirExists(archiveDir)) ? await findArchivedFiles(archiveDir) : [];
|
|
273
|
+
const dossierSourceDir = path.join(ctxDir, 'features', slug);
|
|
274
|
+
const dossierTargetDir = path.join(archiveDir, 'dossier');
|
|
275
|
+
const hasDossierToMove = await dirExists(dossierSourceDir);
|
|
276
|
+
const dossierAlreadyArchived = await dirExists(dossierTargetDir);
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
rootFiles.length === 0 &&
|
|
280
|
+
alreadyArchived.length === 0 &&
|
|
281
|
+
!hasDossierToMove &&
|
|
282
|
+
!dossierAlreadyArchived
|
|
283
|
+
) {
|
|
284
|
+
if (jsonOut) return { ok: true, slug, moved: [], skipped: [], alreadyArchived: [], noop: true };
|
|
285
|
+
log(`No files matched "*-${slug}.{${ARCHIVED_EXTENSIONS.join(',')}}" in .aioson/context/ root and no features/${slug}/ dossier dir — nothing to archive.`);
|
|
286
|
+
return { ok: true, noop: true };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const toMove = [];
|
|
290
|
+
const toSkip = [];
|
|
291
|
+
for (const name of rootFiles) {
|
|
292
|
+
if (alreadyArchived.includes(name)) {
|
|
293
|
+
toSkip.push({ name, reason: 'already_archived' });
|
|
294
|
+
} else {
|
|
295
|
+
toMove.push(name);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const completed = await readCompletedDate(featuresPath, slug) || new Date().toISOString().slice(0, 10);
|
|
300
|
+
const prdName = `prd-${slug}.md`;
|
|
301
|
+
const prdPathInRoot = path.join(ctxDir, prdName);
|
|
302
|
+
const prdPathInArchive = path.join(archiveDir, prdName);
|
|
303
|
+
const summarySource = rootFiles.includes(prdName) ? prdPathInRoot
|
|
304
|
+
: alreadyArchived.includes(prdName) ? prdPathInArchive
|
|
305
|
+
: null;
|
|
306
|
+
const summary = summarySource ? await extractSummary(summarySource) : null;
|
|
307
|
+
|
|
308
|
+
const dossierPlan = hasDossierToMove
|
|
309
|
+
? (dossierAlreadyArchived ? { action: 'skip', reason: 'already_archived' } : { action: 'move' })
|
|
310
|
+
: (dossierAlreadyArchived ? { action: 'noop', reason: 'already_archived' } : null);
|
|
311
|
+
|
|
312
|
+
if (dryRun) {
|
|
313
|
+
const result = {
|
|
314
|
+
ok: true,
|
|
315
|
+
dryRun: true,
|
|
316
|
+
slug,
|
|
317
|
+
targetDir: path.relative(targetDir, archiveDir),
|
|
318
|
+
move: toMove,
|
|
319
|
+
skip: toSkip,
|
|
320
|
+
dossier: dossierPlan
|
|
321
|
+
? {
|
|
322
|
+
source: path.relative(targetDir, dossierSourceDir),
|
|
323
|
+
target: path.relative(targetDir, dossierTargetDir),
|
|
324
|
+
...dossierPlan
|
|
325
|
+
}
|
|
326
|
+
: null,
|
|
327
|
+
manifestEntry: {
|
|
328
|
+
slug,
|
|
329
|
+
completed,
|
|
330
|
+
files: String(toMove.length + alreadyArchived.length),
|
|
331
|
+
summary: summary || '—'
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
if (jsonOut) return result;
|
|
335
|
+
log(`[dry-run] feature:archive — ${slug}:`);
|
|
336
|
+
log(` target: ${path.relative(targetDir, archiveDir)}/`);
|
|
337
|
+
log(` would move: ${toMove.length} file(s)`);
|
|
338
|
+
for (const f of toMove) log(` • ${f}`);
|
|
339
|
+
if (toSkip.length) {
|
|
340
|
+
log(` would skip: ${toSkip.length} file(s)`);
|
|
341
|
+
for (const s of toSkip) log(` • ${s.name} (${s.reason})`);
|
|
342
|
+
}
|
|
343
|
+
if (dossierPlan && dossierPlan.action === 'move') {
|
|
344
|
+
log(` would move dossier dir: features/${slug}/ → ${path.relative(targetDir, dossierTargetDir)}/`);
|
|
345
|
+
} else if (dossierPlan && dossierPlan.action === 'skip') {
|
|
346
|
+
log(` would skip dossier dir: already archived at ${path.relative(targetDir, dossierTargetDir)}/`);
|
|
347
|
+
}
|
|
348
|
+
log(` manifest entry: | ${slug} | ${completed} | ${toMove.length + alreadyArchived.length} | ${summary || '—'} |`);
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
353
|
+
|
|
354
|
+
const moved = [];
|
|
355
|
+
for (const name of toMove) {
|
|
356
|
+
const from = path.join(ctxDir, name);
|
|
357
|
+
const to = path.join(archiveDir, name);
|
|
358
|
+
await fs.rename(from, to);
|
|
359
|
+
moved.push(name);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let dossierResult = null;
|
|
363
|
+
if (dossierPlan && dossierPlan.action === 'move') {
|
|
364
|
+
await fs.rename(dossierSourceDir, dossierTargetDir);
|
|
365
|
+
dossierResult = {
|
|
366
|
+
action: 'moved',
|
|
367
|
+
source: path.relative(targetDir, dossierSourceDir),
|
|
368
|
+
target: path.relative(targetDir, dossierTargetDir)
|
|
369
|
+
};
|
|
370
|
+
try {
|
|
371
|
+
const parent = path.join(ctxDir, 'features');
|
|
372
|
+
const remaining = await fs.readdir(parent);
|
|
373
|
+
if (remaining.length === 0) await fs.rmdir(parent);
|
|
374
|
+
} catch {
|
|
375
|
+
// parent missing or non-empty — leave it
|
|
376
|
+
}
|
|
377
|
+
} else if (dossierPlan && dossierPlan.action === 'skip') {
|
|
378
|
+
dossierResult = {
|
|
379
|
+
action: 'skipped',
|
|
380
|
+
reason: dossierPlan.reason,
|
|
381
|
+
target: path.relative(targetDir, dossierTargetDir)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const totalArchived = (await findArchivedFiles(archiveDir)).length;
|
|
386
|
+
const entry = {
|
|
387
|
+
slug,
|
|
388
|
+
completed,
|
|
389
|
+
files: String(totalArchived),
|
|
390
|
+
summary: summary || '—'
|
|
391
|
+
};
|
|
392
|
+
await updateManifest(manifestPath, entry, 'upsert');
|
|
393
|
+
|
|
394
|
+
const result = {
|
|
395
|
+
ok: true,
|
|
396
|
+
slug,
|
|
397
|
+
completed,
|
|
398
|
+
archiveDir: path.relative(targetDir, archiveDir),
|
|
399
|
+
moved,
|
|
400
|
+
skipped: toSkip,
|
|
401
|
+
totalArchived,
|
|
402
|
+
dossier: dossierResult,
|
|
403
|
+
manifestEntry: entry
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
if (jsonOut) return result;
|
|
407
|
+
log(`feature:archive — ${slug}:`);
|
|
408
|
+
log(` archive dir: ${path.relative(targetDir, archiveDir)}/`);
|
|
409
|
+
log(` moved: ${moved.length} file(s)`);
|
|
410
|
+
for (const f of moved) log(` • ${f}`);
|
|
411
|
+
if (toSkip.length) {
|
|
412
|
+
log(` skipped: ${toSkip.length} file(s) already in archive`);
|
|
413
|
+
for (const s of toSkip) log(` • ${s.name}`);
|
|
414
|
+
}
|
|
415
|
+
if (dossierResult && dossierResult.action === 'moved') {
|
|
416
|
+
log(` moved dossier dir: ${dossierResult.source}/ → ${dossierResult.target}/`);
|
|
417
|
+
} else if (dossierResult && dossierResult.action === 'skipped') {
|
|
418
|
+
log(` skipped dossier dir: already archived at ${dossierResult.target}/`);
|
|
419
|
+
}
|
|
420
|
+
log(` manifest updated: .aioson/context/done/MANIFEST.md`);
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function runRestore({ slug, ctxDir, archiveDir, manifestPath, dryRun, jsonOut, log }) {
|
|
425
|
+
if (!(await dirExists(archiveDir))) {
|
|
426
|
+
if (jsonOut) return { ok: false, reason: 'nothing_to_restore', slug };
|
|
427
|
+
log(`No archive found at .aioson/context/done/${slug}/ — nothing to restore.`);
|
|
428
|
+
return { ok: false };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const dossierTargetDir = path.join(archiveDir, 'dossier');
|
|
432
|
+
const dossierSourceDir = path.join(ctxDir, 'features', slug);
|
|
433
|
+
const hasDossierToRestore = await dirExists(dossierTargetDir);
|
|
434
|
+
const dossierConflict = hasDossierToRestore && (await dirExists(dossierSourceDir));
|
|
435
|
+
|
|
436
|
+
const archived = await findArchivedFiles(archiveDir);
|
|
437
|
+
const conflicts = [];
|
|
438
|
+
const toRestore = [];
|
|
439
|
+
for (const name of archived) {
|
|
440
|
+
const rootPath = path.join(ctxDir, name);
|
|
441
|
+
try {
|
|
442
|
+
await fs.access(rootPath);
|
|
443
|
+
conflicts.push(name);
|
|
444
|
+
} catch {
|
|
445
|
+
toRestore.push(name);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (dossierConflict) conflicts.push(`features/${slug}/`);
|
|
449
|
+
|
|
450
|
+
if (conflicts.length > 0) {
|
|
451
|
+
if (jsonOut) return { ok: false, reason: 'restore_conflict', slug, conflicts };
|
|
452
|
+
log(`Cannot restore "${slug}" — files already exist in .aioson/context/ root:`);
|
|
453
|
+
for (const c of conflicts) log(` • ${c}`);
|
|
454
|
+
log(`Resolve manually before retrying --restore.`);
|
|
455
|
+
return { ok: false };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (dryRun) {
|
|
459
|
+
const result = {
|
|
460
|
+
ok: true,
|
|
461
|
+
dryRun: true,
|
|
462
|
+
slug,
|
|
463
|
+
restore: toRestore,
|
|
464
|
+
dossier: hasDossierToRestore ? { action: 'restore', target: path.relative(ctxDir, dossierSourceDir) } : null
|
|
465
|
+
};
|
|
466
|
+
if (jsonOut) return result;
|
|
467
|
+
log(`[dry-run] feature:archive --restore — ${slug}:`);
|
|
468
|
+
log(` would restore: ${toRestore.length} file(s)`);
|
|
469
|
+
for (const f of toRestore) log(` • ${f}`);
|
|
470
|
+
if (hasDossierToRestore) log(` would restore dossier dir: ${path.relative(ctxDir, dossierTargetDir)}/ → features/${slug}/`);
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const restored = [];
|
|
475
|
+
for (const name of toRestore) {
|
|
476
|
+
const from = path.join(archiveDir, name);
|
|
477
|
+
const to = path.join(ctxDir, name);
|
|
478
|
+
await fs.rename(from, to);
|
|
479
|
+
restored.push(name);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let dossierRestored = null;
|
|
483
|
+
if (hasDossierToRestore) {
|
|
484
|
+
await fs.mkdir(path.dirname(dossierSourceDir), { recursive: true });
|
|
485
|
+
await fs.rename(dossierTargetDir, dossierSourceDir);
|
|
486
|
+
dossierRestored = path.relative(ctxDir, dossierSourceDir);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
await fs.rmdir(archiveDir);
|
|
491
|
+
} catch {
|
|
492
|
+
// Directory not empty (manual files) — leave it alone.
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
await updateManifest(manifestPath, { slug }, 'remove');
|
|
496
|
+
|
|
497
|
+
const result = {
|
|
498
|
+
ok: true,
|
|
499
|
+
slug,
|
|
500
|
+
restored,
|
|
501
|
+
dossierRestored,
|
|
502
|
+
archiveDir: path.relative(ctxDir, archiveDir)
|
|
503
|
+
};
|
|
504
|
+
if (jsonOut) return result;
|
|
505
|
+
log(`feature:archive --restore — ${slug}:`);
|
|
506
|
+
log(` restored: ${restored.length} file(s)`);
|
|
507
|
+
for (const f of restored) log(` • ${f}`);
|
|
508
|
+
if (dossierRestored) log(` restored dossier dir: ${dossierRestored}/`);
|
|
509
|
+
log(` manifest updated: .aioson/context/done/MANIFEST.md`);
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
module.exports = { runFeatureArchive };
|
|
@@ -15,11 +15,83 @@
|
|
|
15
15
|
const fs = require('node:fs/promises');
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
const { contextDir, readFileSafe, parseFrontmatter } = require('../preflight-engine');
|
|
18
|
+
const { runFeatureArchive } = require('./feature-archive');
|
|
18
19
|
|
|
19
20
|
function nowDate() {
|
|
20
21
|
return new Date().toISOString().slice(0, 10);
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function nowTimestamp() {
|
|
25
|
+
return new Date().toISOString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function quoteYaml(value) {
|
|
29
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractRecentActivities(content) {
|
|
33
|
+
if (!content) return [];
|
|
34
|
+
const activityMatch = content.match(/## Recent Activity\n([\s\S]*?)(?=\n##|\s*$)/);
|
|
35
|
+
if (!activityMatch) return [];
|
|
36
|
+
return activityMatch[1]
|
|
37
|
+
.split('\n')
|
|
38
|
+
.filter((line) => line.trim().startsWith('-'))
|
|
39
|
+
.slice(-2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function updateProjectPulseFile(pulsePath, slug, verdict, summary, date) {
|
|
43
|
+
const existing = await readFileSafe(pulsePath);
|
|
44
|
+
if (!existing) return false;
|
|
45
|
+
|
|
46
|
+
const fm = parseFrontmatter(existing);
|
|
47
|
+
const gate = `Gate D: ${verdict === 'PASS' ? 'approved' : 'rejected'}`;
|
|
48
|
+
const recentActivities = extractRecentActivities(existing);
|
|
49
|
+
let activityLine = `- ${date} @qa → ${slug} (${gate}) VERDICT: ${verdict}`;
|
|
50
|
+
if (summary) activityLine += `: ${summary}`;
|
|
51
|
+
const dedupedActivities = recentActivities.filter((line) => line !== activityLine);
|
|
52
|
+
|
|
53
|
+
const activeFeature = verdict === 'PASS' ? '(none)' : slug;
|
|
54
|
+
const activeWork = verdict === 'PASS' ? '' : `${slug} → @qa → qa_failed`;
|
|
55
|
+
const blockers = verdict === 'PASS'
|
|
56
|
+
? 'none'
|
|
57
|
+
: (summary || fm.blockers || 'QA blockers pending');
|
|
58
|
+
const nextRecommendation = verdict === 'PASS'
|
|
59
|
+
? '@product start the next feature'
|
|
60
|
+
: '@dev fix QA blockers and return to @qa';
|
|
61
|
+
|
|
62
|
+
const lines = [
|
|
63
|
+
'---',
|
|
64
|
+
`last_updated: ${nowTimestamp()}`,
|
|
65
|
+
'last_agent: qa',
|
|
66
|
+
`last_gate: ${gate}`,
|
|
67
|
+
`active_feature: ${activeFeature}`,
|
|
68
|
+
`active_work: ${quoteYaml(activeWork)}`,
|
|
69
|
+
`blockers: ${quoteYaml(blockers)}`,
|
|
70
|
+
`next_recommendation: ${quoteYaml(nextRecommendation)}`,
|
|
71
|
+
'---',
|
|
72
|
+
'',
|
|
73
|
+
'# Project Pulse',
|
|
74
|
+
'',
|
|
75
|
+
'## Status',
|
|
76
|
+
'',
|
|
77
|
+
'- **Last agent:** @qa',
|
|
78
|
+
`- **Last gate:** ${gate}`,
|
|
79
|
+
`- **Active feature:** ${activeFeature}`,
|
|
80
|
+
`- **Active work:** ${activeWork || 'none'}`,
|
|
81
|
+
`- **Blockers:** ${blockers}`,
|
|
82
|
+
`- **Next:** ${nextRecommendation}`,
|
|
83
|
+
'',
|
|
84
|
+
'## Recent Activity',
|
|
85
|
+
'',
|
|
86
|
+
...dedupedActivities,
|
|
87
|
+
activityLine,
|
|
88
|
+
''
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
await fs.writeFile(pulsePath, lines.join('\n'), 'utf8');
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
23
95
|
async function updateSpecFile(specPath, verdict, residual, date) {
|
|
24
96
|
const content = await readFileSafe(specPath);
|
|
25
97
|
if (!content) return false;
|
|
@@ -65,26 +137,34 @@ async function updateSpecFile(specPath, verdict, residual, date) {
|
|
|
65
137
|
return true;
|
|
66
138
|
}
|
|
67
139
|
|
|
140
|
+
function escapeSlugForRegex(slug) {
|
|
141
|
+
return slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
|
+
}
|
|
143
|
+
|
|
68
144
|
async function updateFeaturesFile(featuresPath, slug, verdict, date) {
|
|
69
145
|
const content = await readFileSafe(featuresPath);
|
|
70
146
|
if (!content) return false;
|
|
71
147
|
|
|
72
148
|
const status = verdict === 'PASS' ? 'done' : 'qa_failed';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
new RegExp(`(\\|[^|]*${slug}[^|]*\\|[^|]*\\|)[^|]*(\\|)`, 'g'),
|
|
77
|
-
(match, before, after) => `${before} ${status} (${date}) ${after}`
|
|
149
|
+
const rowRe = new RegExp(
|
|
150
|
+
`^(\\|\\s*${escapeSlugForRegex(slug)}\\s*\\|)\\s*[^|]*\\s*\\|\\s*([^|]*)\\s*\\|\\s*([^|]*)\\s*\\|(.*)$`,
|
|
151
|
+
'm'
|
|
78
152
|
);
|
|
79
153
|
|
|
154
|
+
const updated = content.replace(rowRe, (match, slugCol, startedCol, _completedCol, rest) => {
|
|
155
|
+
const started = startedCol.trim() || date;
|
|
156
|
+
return `${slugCol} ${status} | ${started} | ${date} |${rest}`;
|
|
157
|
+
});
|
|
158
|
+
|
|
80
159
|
if (updated !== content) {
|
|
81
160
|
await fs.writeFile(featuresPath, updated, 'utf8');
|
|
82
161
|
return true;
|
|
83
162
|
}
|
|
84
163
|
|
|
85
164
|
// Append if not found
|
|
86
|
-
const line = `| ${slug} | ${
|
|
87
|
-
|
|
165
|
+
const line = `| ${slug} | ${status} | ${date} | ${date} |`;
|
|
166
|
+
const needsNewline = !content.endsWith('\n');
|
|
167
|
+
await fs.appendFile(featuresPath, `${needsNewline ? '\n' : ''}${line}\n`, 'utf8');
|
|
88
168
|
return true;
|
|
89
169
|
}
|
|
90
170
|
|
|
@@ -132,17 +212,41 @@ async function runFeatureClose({ args, options = {}, logger }) {
|
|
|
132
212
|
|
|
133
213
|
// 3. Update project-pulse.md
|
|
134
214
|
const pulsePath = path.join(dir, 'project-pulse.md');
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.replace(/last_gate:\s*.+/, `last_gate: Gate D: ${verdict === 'PASS' ? 'approved' : 'rejected'}`);
|
|
144
|
-
await fs.writeFile(pulsePath, updatedPulse, 'utf8');
|
|
215
|
+
const pulseUpdated = await updateProjectPulseFile(
|
|
216
|
+
pulsePath,
|
|
217
|
+
slug,
|
|
218
|
+
verdict,
|
|
219
|
+
residual || notes || null,
|
|
220
|
+
today
|
|
221
|
+
);
|
|
222
|
+
if (pulseUpdated) {
|
|
145
223
|
updates.push('project-pulse.md: updated active work');
|
|
224
|
+
} else {
|
|
225
|
+
updates.push('project-pulse.md: not found (skipped)');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 4. Auto-archive on PASS (default-on — user never has to remember).
|
|
229
|
+
// Disable explicitly with --no-archive when needed (e.g. re-running feature:close idempotently).
|
|
230
|
+
let archive = null;
|
|
231
|
+
const skipArchive = options['no-archive'] === true || options.archive === false;
|
|
232
|
+
if (verdict === 'PASS' && !skipArchive) {
|
|
233
|
+
try {
|
|
234
|
+
archive = await runFeatureArchive({
|
|
235
|
+
args: [targetDir],
|
|
236
|
+
options: { feature: slug, json: true },
|
|
237
|
+
logger: null
|
|
238
|
+
});
|
|
239
|
+
if (archive && archive.ok && archive.moved && archive.moved.length > 0) {
|
|
240
|
+
updates.push(`archive: moved ${archive.moved.length} file(s) to ${archive.archiveDir}/`);
|
|
241
|
+
updates.push(`archive: manifest updated at .aioson/context/done/MANIFEST.md`);
|
|
242
|
+
} else if (archive && archive.ok && archive.noop) {
|
|
243
|
+
updates.push('archive: nothing to move (already clean)');
|
|
244
|
+
} else if (archive && !archive.ok) {
|
|
245
|
+
updates.push(`archive: skipped (${archive.reason || 'unknown'})`);
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
updates.push(`archive: failed (${err.message || err})`);
|
|
249
|
+
}
|
|
146
250
|
}
|
|
147
251
|
|
|
148
252
|
const result = {
|
|
@@ -151,7 +255,8 @@ async function runFeatureClose({ args, options = {}, logger }) {
|
|
|
151
255
|
verdict,
|
|
152
256
|
date: today,
|
|
153
257
|
residual: residual || notes || null,
|
|
154
|
-
updates
|
|
258
|
+
updates,
|
|
259
|
+
archive
|
|
155
260
|
};
|
|
156
261
|
|
|
157
262
|
if (options.json) return result;
|