@thebackstoryis/engineering-with-ai 0.2.9
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/Docs/README.md +50 -0
- package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
- package/Docs/adoption/non-technical-team-guide.md +126 -0
- package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
- package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
- package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
- package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
- package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
- package/Docs/cli-reference.md +113 -0
- package/Docs/completed-phase-evidence-amendments.md +74 -0
- package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
- package/Docs/context-aware-delivery-companion-guide.md +198 -0
- package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
- package/Docs/context-management-and-token-efficiency.md +113 -0
- package/Docs/design-systems/design-system-implementation-guide.md +85 -0
- package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
- package/Docs/design-systems/design-system-review-guide.md +51 -0
- package/Docs/design-systems/design-system-user-guide.md +96 -0
- package/Docs/design-systems/product-owner-guide.md +49 -0
- package/Docs/designing-organisation-blueprint-packs.md +384 -0
- package/Docs/developer-delivery-guide.md +224 -0
- package/Docs/error-reporting-guide.md +110 -0
- package/Docs/error-reporting-provider-guide.md +49 -0
- package/Docs/examples/error-report-adapter.md +70 -0
- package/Docs/examples/meeting-review.md +76 -0
- package/Docs/examples/minimal-design-system.md +67 -0
- package/Docs/examples/prototype-review-inputs.md +175 -0
- package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
- package/Docs/examples/test-scenario-input.md +68 -0
- package/Docs/examples/worked-examples.md +147 -0
- package/Docs/existing-project-onboarding-guide.md +214 -0
- package/Docs/explanation/core-concepts.md +26 -0
- package/Docs/explanation/delivery-workflow.md +48 -0
- package/Docs/governance/governance-team-guide.md +139 -0
- package/Docs/governed-starter-project-materialisation-guide.md +284 -0
- package/Docs/guide-catalogue.md +117 -0
- package/Docs/guided-discovery-facilitator-guide.md +172 -0
- package/Docs/guided-intent-workspace-guide.md +109 -0
- package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
- package/Docs/human-approval-and-assurance-guide.md +146 -0
- package/Docs/knowledge-proposals-implementer-guide.md +106 -0
- package/Docs/knowledge-proposals-user-guide.md +247 -0
- package/Docs/maintainers/context-benchmarks.md +29 -0
- package/Docs/maintainers/contributing.md +58 -0
- package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
- package/Docs/maintainers/verification-walkthroughs.md +104 -0
- package/Docs/meeting-evidence-implementer-guide.md +132 -0
- package/Docs/meeting-evidence-user-guide.md +200 -0
- package/Docs/operations/dashboard-and-delivery-state.md +153 -0
- package/Docs/operations/dashboard-configuration.md +87 -0
- package/Docs/operations/installation-updating-and-entitlements.md +135 -0
- package/Docs/operations/premium-personas-setup.md +76 -0
- package/Docs/operations/troubleshooting-and-recovery.md +205 -0
- package/Docs/organisation-rollout-guide.md +142 -0
- package/Docs/persona-entitlement-provider-guide.md +199 -0
- package/Docs/persona-guided-prototype-iteration.md +129 -0
- package/Docs/personas/organisation-specific-personas.md +103 -0
- package/Docs/personas/persona-authoring-cookbook.md +176 -0
- package/Docs/personas/persona-engagement-ui.md +133 -0
- package/Docs/personas/persona-governance.md +118 -0
- package/Docs/platform-export-analysis-guide.md +336 -0
- package/Docs/policies/governance-owner-guide.md +36 -0
- package/Docs/policies/implementation-guide.md +42 -0
- package/Docs/policies/organisation-policy-design-gates.md +58 -0
- package/Docs/policies/policy-pack-authoring-guide.md +108 -0
- package/Docs/policies/product-owner-guide.md +43 -0
- package/Docs/policies/technical-owner-guide.md +37 -0
- package/Docs/product-owner-guide.md +327 -0
- package/Docs/project-portfolio-orchestration-guide.md +199 -0
- package/Docs/quality/manual-qa-and-acceptance.md +162 -0
- package/Docs/quality/persona-driven-test-scenarios.md +172 -0
- package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
- package/Docs/reference/capabilities-and-project-layout.md +678 -0
- package/Docs/reference/cli-and-configuration.md +398 -0
- package/Docs/reference/contributions-api.md +23 -0
- package/Docs/reference/security-adapter-authoring.md +81 -0
- package/Docs/reference/starter-adapter-authoring.md +74 -0
- package/Docs/repository-source-map-guide.md +381 -0
- package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
- package/Docs/screen-prototype-creation-guide.md +324 -0
- package/Docs/security-validation-guide.md +353 -0
- package/Docs/solution-readiness-review-guide.md +123 -0
- package/Docs/standards/project-standards-authoring.md +157 -0
- package/Docs/team-hub-guide.md +162 -0
- package/Docs/team-hub-resource-registry-guide.md +167 -0
- package/Docs/tutorials/first-delivery.md +83 -0
- package/Docs/tutorials/first-session.md +62 -0
- package/Docs/using-lifecycle-hooks.md +381 -0
- package/Docs/working-with-personas.md +274 -0
- package/LICENSE +165 -0
- package/README.md +96 -0
- package/agents-src/claude/ewai-security-reviewer.md +15 -0
- package/bin/ewai +5 -0
- package/config/archaeology-record-families.yaml +59 -0
- package/config/delivery-artifacts.yaml +121 -0
- package/config/delivery-stages.yaml +77 -0
- package/config/design-system.schema.json +46 -0
- package/config/error-reporting.schema.json +79 -0
- package/config/evidence-depth.schema.json +53 -0
- package/config/intent.schema.json +90 -0
- package/config/knowledge-proposals-proposal.schema.json +34 -0
- package/config/lifecycle-event.schema.json +68 -0
- package/config/lifecycle-handler.schema.json +45 -0
- package/config/lifecycle-hook-ack.schema.json +19 -0
- package/config/meeting-evidence-candidate.schema.json +102 -0
- package/config/organisation-policy.schema.json +137 -0
- package/config/pack.schema.json +250 -0
- package/config/persona-pack.schema.json +21 -0
- package/config/persona.schema.json +17 -0
- package/config/policy-evaluation.schema.json +77 -0
- package/config/policy-facts.schema.json +140 -0
- package/config/portfolio.schema.json +68 -0
- package/config/project.schema.json +313 -0
- package/config/prototype-iteration.schema.json +128 -0
- package/config/rollout.schema.json +87 -0
- package/config/security-adapter.schema.json +31 -0
- package/config/security-scan-request.schema.json +64 -0
- package/config/security-scan-response.schema.json +52 -0
- package/config/security-validation-policy.schema.json +74 -0
- package/config/starter-source-acknowledgement.schema.json +13 -0
- package/config/starter-source-adapter.schema.json +38 -0
- package/config/starter-source-request.schema.json +61 -0
- package/package.json +77 -0
- package/packs/core/pack.yaml +7 -0
- package/packs/design-systems/default/experience-promise.md +9 -0
- package/packs/design-systems/default/intentional-review.md +10 -0
- package/packs/design-systems/default/interaction-and-entry.md +9 -0
- package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
- package/packs/design-systems/default/pack.yaml +44 -0
- package/packs/design-systems/default/principles.md +10 -0
- package/packs/personas/core/pack.yaml +7 -0
- package/packs/personas/core/personas/archaeologist.md +37 -0
- package/packs/personas/core/personas/end-user.md +17 -0
- package/packs/personas/core/personas/maintainer.md +17 -0
- package/packs/personas/core/personas/operator.md +17 -0
- package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
- package/packs/technologies/laravel/pack.yaml +30 -0
- package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
- package/packs/technologies/nuxt/pack.yaml +30 -0
- package/packs/technologies/power-platform/pack.yaml +31 -0
- package/packs/technologies/salesforce/pack.yaml +25 -0
- package/public/app.js +4896 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/backstory-icon.png +0 -0
- package/public/dashboard-navigation.js +98 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +789 -0
- package/public/styles.css +2693 -0
- package/public/team-hub/app.js +202 -0
- package/public/team-hub/index.html +79 -0
- package/public/team-hub/styles.css +90 -0
- package/scripts/publication-check.mjs +140 -0
- package/scripts/setup.mjs +21 -0
- package/skills-src/ewai-archaeology/SKILL.md +334 -0
- package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
- package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
- package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
- package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
- package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
- package/skills-src/ewai-architecture/SKILL.md +108 -0
- package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
- package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
- package/skills-src/ewai-context/SKILL.md +68 -0
- package/skills-src/ewai-context/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/SKILL.md +118 -0
- package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
- package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
- package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
- package/skills-src/ewai-deliver/SKILL.md +122 -0
- package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
- package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
- package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
- package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
- package/skills-src/ewai-design-system-author/SKILL.md +28 -0
- package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
- package/skills-src/ewai-design-system-review/SKILL.md +26 -0
- package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
- package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
- package/skills-src/ewai-error-reporting/SKILL.md +46 -0
- package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
- package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
- package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
- package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
- package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
- package/skills-src/ewai-intent/SKILL.md +68 -0
- package/skills-src/ewai-intent/agents/openai.yaml +4 -0
- package/skills-src/ewai-intent/references/intent-contract.md +42 -0
- package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
- package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
- package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
- package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
- package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
- package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
- package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
- package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
- package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
- package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
- package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
- package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
- package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
- package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/SKILL.md +130 -0
- package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
- package/skills-src/ewai-pipeline/references/cli.md +86 -0
- package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
- package/skills-src/ewai-portfolio/SKILL.md +70 -0
- package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
- package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
- package/skills-src/ewai-project-discovery/SKILL.md +95 -0
- package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
- package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
- package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
- package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
- package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
- package/skills-src/ewai-retro/SKILL.md +48 -0
- package/skills-src/ewai-retro/agents/openai.yaml +4 -0
- package/skills-src/ewai-retro/references/asset-routing.md +14 -0
- package/skills-src/ewai-rollout/SKILL.md +74 -0
- package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
- package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
- package/skills-src/ewai-shape-intents/SKILL.md +84 -0
- package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
- package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
- package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
- package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/SKILL.md +93 -0
- package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
- package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
- package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
- package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
- package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
- package/src/afk-worker.mjs +16 -0
- package/src/archaeology.mjs +1333 -0
- package/src/checkin.mjs +261 -0
- package/src/cli.mjs +2427 -0
- package/src/companion-guidance.mjs +257 -0
- package/src/companion-opening.mjs +62 -0
- package/src/companion.mjs +256 -0
- package/src/context.mjs +210 -0
- package/src/dashboard-preferences.mjs +80 -0
- package/src/delivery-artifacts.mjs +204 -0
- package/src/delivery-documents.mjs +248 -0
- package/src/delivery-gates.mjs +317 -0
- package/src/delivery.mjs +1433 -0
- package/src/design-system-application.mjs +291 -0
- package/src/design-system-authoring.mjs +101 -0
- package/src/design-systems.mjs +466 -0
- package/src/discovery.mjs +1314 -0
- package/src/error-reporting.mjs +323 -0
- package/src/evidence-depth.mjs +543 -0
- package/src/execution-state.mjs +243 -0
- package/src/install.mjs +166 -0
- package/src/intent-dependencies.mjs +117 -0
- package/src/intent-maps.mjs +402 -0
- package/src/intents.mjs +747 -0
- package/src/knowledge-proposals.mjs +717 -0
- package/src/launcher.mjs +51 -0
- package/src/meeting-evidence.mjs +703 -0
- package/src/network-rollout.mjs +386 -0
- package/src/organisation-blueprints.mjs +438 -0
- package/src/organisation-policies.mjs +448 -0
- package/src/packs.mjs +44 -0
- package/src/paths.mjs +62 -0
- package/src/persona-entitlements.mjs +438 -0
- package/src/persona-licence-config.mjs +98 -0
- package/src/persona-website-provider.mjs +134 -0
- package/src/persona-zip.mjs +87 -0
- package/src/personas.mjs +159 -0
- package/src/platform-metadata-analysis.mjs +314 -0
- package/src/policy-design-gates.mjs +623 -0
- package/src/policy-gate-integration.mjs +318 -0
- package/src/portfolio.mjs +509 -0
- package/src/power-platform-source-map.mjs +190 -0
- package/src/project.mjs +449 -0
- package/src/prototype-iterations.mjs +730 -0
- package/src/repository-source-map.mjs +603 -0
- package/src/runtime/afk-conductor.mjs +973 -0
- package/src/runtime/context-assembly.mjs +457 -0
- package/src/runtime/context-benchmarks.mjs +115 -0
- package/src/runtime/dashboard-actions.mjs +109 -0
- package/src/runtime/dashboard-handoffs.mjs +149 -0
- package/src/runtime/dashboard-server.mjs +1272 -0
- package/src/runtime/dashboard.mjs +197 -0
- package/src/runtime/database.mjs +789 -0
- package/src/runtime/error-reporting.mjs +581 -0
- package/src/runtime/evidence-depth-workspace.mjs +412 -0
- package/src/runtime/execution-leases.mjs +299 -0
- package/src/runtime/guided-discovery.mjs +350 -0
- package/src/runtime/guided-intents.mjs +517 -0
- package/src/runtime/impact-analysis.mjs +535 -0
- package/src/runtime/intents.mjs +222 -0
- package/src/runtime/knowledge.mjs +86 -0
- package/src/runtime/lifecycle-hooks.mjs +1239 -0
- package/src/runtime/mcp-config.mjs +110 -0
- package/src/runtime/mcp-server.mjs +1885 -0
- package/src/runtime/palace.mjs +362 -0
- package/src/runtime/paths.mjs +58 -0
- package/src/runtime/persona-engagement.mjs +255 -0
- package/src/runtime/phase-contributions.mjs +594 -0
- package/src/runtime/policy-workspace.mjs +170 -0
- package/src/runtime/prototype-iterations.mjs +235 -0
- package/src/runtime/provider-adapters.mjs +163 -0
- package/src/runtime/repository-index.mjs +838 -0
- package/src/runtime/runs.mjs +185 -0
- package/src/runtime/security-validation.mjs +1230 -0
- package/src/runtime/starter-materialisation.mjs +1155 -0
- package/src/runtime/team-hub-client.mjs +479 -0
- package/src/runtime/team-hub-database.mjs +288 -0
- package/src/runtime/team-hub-server.mjs +191 -0
- package/src/runtime/team-hub.mjs +110 -0
- package/src/runtime/tree-sitter-index.mjs +390 -0
- package/src/runtime/version.mjs +1 -0
- package/src/runtime/work.mjs +633 -0
- package/src/salesforce-source-map.mjs +212 -0
- package/src/security-validation-config.mjs +224 -0
- package/src/solution-readiness.mjs +620 -0
- package/src/starter-materialisation-contract.mjs +407 -0
- package/src/task-graph.mjs +544 -0
- package/src/team-hub-resources.mjs +239 -0
- package/src/team-hub.mjs +242 -0
- package/src/test-scenarios.mjs +622 -0
- package/src/validation-config.mjs +289 -0
- package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
- package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
- package/templates/SPECS/6.Build/_tracker-template.md +16 -0
- package/templates/SPECS/pipeline.yaml +62 -0
- package/templates/discovery-answers.yaml +86 -0
- package/templates/intent-body.md +21 -0
- package/tests/fixtures/context-benchmarks.json +9 -0
package/src/delivery.mjs
ADDED
|
@@ -0,0 +1,1433 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmdirSync,
|
|
8
|
+
rmSync
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
import { updateIntentDeliveryState, validateIntentStateCopies } from './intents.mjs';
|
|
14
|
+
import { loadProjectConfig } from './project.mjs';
|
|
15
|
+
import { readRuntimeIntent } from './runtime/intents.mjs';
|
|
16
|
+
import {
|
|
17
|
+
addActivityEvent,
|
|
18
|
+
finishActiveSession,
|
|
19
|
+
readWorkItemView,
|
|
20
|
+
setPhase,
|
|
21
|
+
startActiveSession,
|
|
22
|
+
updateWorkItem
|
|
23
|
+
} from './runtime/work.mjs';
|
|
24
|
+
import { finishCommandRun, startCommandRun } from './runtime/runs.mjs';
|
|
25
|
+
import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
|
|
26
|
+
import { assessSecurityReadiness, captureSecurityPolicy } from './runtime/security-validation.mjs';
|
|
27
|
+
import {
|
|
28
|
+
atomicJson,
|
|
29
|
+
atomicText,
|
|
30
|
+
deliveryPaths,
|
|
31
|
+
isWithin,
|
|
32
|
+
now,
|
|
33
|
+
sha256,
|
|
34
|
+
writeDeliveryDocuments
|
|
35
|
+
} from './delivery-documents.mjs';
|
|
36
|
+
import { validatePhaseArtefacts } from './delivery-artifacts.mjs';
|
|
37
|
+
import {
|
|
38
|
+
canonicalValidationProvider,
|
|
39
|
+
effectiveValidationConfig,
|
|
40
|
+
VALIDATION_PROVIDERS,
|
|
41
|
+
validationCheckpointForPhase,
|
|
42
|
+
} from './validation-config.mjs';
|
|
43
|
+
import {
|
|
44
|
+
assertCompletedEvidenceFresh,
|
|
45
|
+
evidenceAbsolute,
|
|
46
|
+
phaseGateTemplateAtPaths,
|
|
47
|
+
readPassingGate,
|
|
48
|
+
recordPhaseGateAtPaths,
|
|
49
|
+
validatePhaseGateAtPaths
|
|
50
|
+
} from './delivery-gates.mjs';
|
|
51
|
+
|
|
52
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
53
|
+
const deliveryContract = YAML.parse(readFileSync(resolve(moduleDir, '../config/delivery-stages.yaml'), 'utf8'));
|
|
54
|
+
const terminalPhaseStatuses = new Set(['completed', 'approved', 'skipped', 'not-supported', 'waived']);
|
|
55
|
+
const acceptedIntentStatuses = new Set(['ready', 'approved']);
|
|
56
|
+
|
|
57
|
+
function newPhase(stage, options, validation) {
|
|
58
|
+
let status = 'pending';
|
|
59
|
+
let reason = '';
|
|
60
|
+
const checkpointName = validationCheckpointForPhase(stage.id);
|
|
61
|
+
const checkpoint = checkpointName ? validation.checkpoints[checkpointName] : null;
|
|
62
|
+
if (stage.id === 'ideate' && !options.ideate) {
|
|
63
|
+
status = 'skipped';
|
|
64
|
+
reason = 'An intent already exists.';
|
|
65
|
+
} else if (stage.id === 'reconcile' && !options.existingCode) {
|
|
66
|
+
status = 'skipped';
|
|
67
|
+
reason = 'No existing implementation is in scope.';
|
|
68
|
+
} else if (stage.required === 'provider-gated' && !checkpoint?.enabled) {
|
|
69
|
+
status = 'not-supported';
|
|
70
|
+
reason = 'This external validation checkpoint is disabled in the project configuration.';
|
|
71
|
+
} else if (stage.required === 'provider-gated' && !checkpoint?.validators.length) {
|
|
72
|
+
status = 'not-supported';
|
|
73
|
+
reason = validation.orchestrator === 'manual'
|
|
74
|
+
? 'No independent external validation provider is enabled for this checkpoint.'
|
|
75
|
+
: `No independent external validator remains after excluding the ${validation.orchestrator} orchestrator.`;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
number: stage.number,
|
|
79
|
+
id: stage.id,
|
|
80
|
+
required: stage.required,
|
|
81
|
+
condition: stage.condition ?? null,
|
|
82
|
+
approval: stage.approval ?? null,
|
|
83
|
+
status,
|
|
84
|
+
statusReason: reason,
|
|
85
|
+
startedAt: null,
|
|
86
|
+
completedAt: status === 'skipped' || status === 'not-supported' ? now() : null,
|
|
87
|
+
gatePath: null,
|
|
88
|
+
gateSha256: null,
|
|
89
|
+
artefactPath: null,
|
|
90
|
+
iterations: 0,
|
|
91
|
+
validation: checkpoint
|
|
92
|
+
? {
|
|
93
|
+
checkpoint: checkpointName,
|
|
94
|
+
enabled: checkpoint.enabled,
|
|
95
|
+
maxCycles: checkpoint.max_cycles,
|
|
96
|
+
providers: [...checkpoint.validators],
|
|
97
|
+
breadth: checkpoint.review.breadth,
|
|
98
|
+
depth: checkpoint.review.depth,
|
|
99
|
+
output: checkpoint.review.output,
|
|
100
|
+
cycles: [],
|
|
101
|
+
}
|
|
102
|
+
: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function assertExternalValidationComplete(phase) {
|
|
107
|
+
for (const provider of phase.validation.providers) {
|
|
108
|
+
const cycles = phase.validation.cycles.filter((cycle) => cycle.provider === provider);
|
|
109
|
+
if (!cycles.length) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Phase ${phase.id} requires at least one recorded validation cycle from ${provider}.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const latest = cycles.at(-1);
|
|
115
|
+
if (latest.outcome !== 'pass') {
|
|
116
|
+
const exhausted = cycles.length >= phase.validation.maxCycles;
|
|
117
|
+
throw new Error(exhausted
|
|
118
|
+
? `Phase ${phase.id} is blocked: ${provider} still reports issues after the configured ${phase.validation.maxCycles} cycle(s).`
|
|
119
|
+
: `Phase ${phase.id} cannot complete: fix ${provider}'s findings and run validation cycle ${cycles.length + 1} of ${phase.validation.maxCycles}.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validationEvidencePath(paths, relativePath) {
|
|
125
|
+
const absolute = resolve(paths.deliveryRoot, relativePath);
|
|
126
|
+
if (!isWithin(paths.deliveryRoot, absolute)) {
|
|
127
|
+
throw new Error(`Validation evidence must remain inside ${paths.deliveryRoot}`);
|
|
128
|
+
}
|
|
129
|
+
if (!existsSync(absolute)) throw new Error(`Validation evidence does not exist: ${absolute}`);
|
|
130
|
+
return absolute;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function phaseSequence(state) {
|
|
134
|
+
const result = [];
|
|
135
|
+
for (const phase of state.phases) {
|
|
136
|
+
result.push(phase);
|
|
137
|
+
if (phase.id === 'reconcile') result.push(state.adjuncts.find((item) => item.id === 'ui-design'));
|
|
138
|
+
if (phase.id === 'build') result.splice(result.length - 1, 0, state.adjuncts.find((item) => item.id === 'fit-check'));
|
|
139
|
+
if (phase.id === 'retro') result.splice(result.length - 1, 0, state.humanGates.find((item) => item.id === 'manual-qa'));
|
|
140
|
+
}
|
|
141
|
+
return result.filter(Boolean);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function nextRequiredPhase(state) {
|
|
145
|
+
const running = phaseSequence(state).find((phase) => phase.status === 'running');
|
|
146
|
+
if (running) return running.id;
|
|
147
|
+
const next = phaseSequence(state).find((phase) => !terminalPhaseStatuses.has(phase.status));
|
|
148
|
+
return next?.id ?? 'complete';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findTrackedPhase(state, phaseId) {
|
|
152
|
+
const phase = [...state.phases, ...state.adjuncts, ...state.humanGates].find((candidate) => candidate.id === phaseId);
|
|
153
|
+
if (!phase) throw new Error(`Unknown EWAI delivery phase: ${phaseId}`);
|
|
154
|
+
return phase;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function progressFor(state) {
|
|
158
|
+
const applicable = state.phases.filter((phase) => !['skipped', 'not-supported'].includes(phase.status));
|
|
159
|
+
const completed = applicable.filter((phase) => phase.status === 'completed').length;
|
|
160
|
+
return applicable.length ? Math.round((completed / applicable.length) * 100) : 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function syncOperationalProjection(paths, state) {
|
|
164
|
+
const next = ['shelf-ready', 'complete-dry-run'].includes(state.currentPhase)
|
|
165
|
+
? state.currentPhase
|
|
166
|
+
: nextRequiredPhase(state);
|
|
167
|
+
const shelfReady = next === 'shelf-ready';
|
|
168
|
+
const finished = next === 'complete' || next === 'complete-dry-run';
|
|
169
|
+
const waiting = next === 'manual-qa';
|
|
170
|
+
for (const tracked of [...state.phases, ...state.adjuncts, ...state.humanGates]) {
|
|
171
|
+
setPhase(paths.projectRoot, state.intent.id, tracked.id, {
|
|
172
|
+
status: tracked.status,
|
|
173
|
+
startedAt: tracked.startedAt,
|
|
174
|
+
completedAt: tracked.completedAt,
|
|
175
|
+
artefactPath: tracked.artefactPath ?? tracked.evidencePath ?? '',
|
|
176
|
+
iterationCount: tracked.iterations ?? 0,
|
|
177
|
+
notes: tracked.statusReason ?? ''
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return updateWorkItem(paths.projectRoot, state.intent.id, {
|
|
181
|
+
lane: finished ? 'done' : shelfReady ? 'ready' : waiting ? 'qa' : 'active',
|
|
182
|
+
state: state.status,
|
|
183
|
+
intentState: state.intent.status,
|
|
184
|
+
completionPercent: progressFor(state),
|
|
185
|
+
currentSprint: !finished && !shelfReady,
|
|
186
|
+
currentPhase: next,
|
|
187
|
+
blockedBy: state.blockedReason ?? '',
|
|
188
|
+
notes: `Durable delivery state: SPECS/6.Build/${state.slug}/delivery-state.json`
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function persistedIntentStatus(state) {
|
|
193
|
+
if (state.currentPhase === 'complete') return 'delivered';
|
|
194
|
+
if (state.phases.find((phase) => phase.id === 'intent')?.status === 'completed') return 'in-progress';
|
|
195
|
+
return state.intent.status;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function lifecycleScope(state, phase = '') {
|
|
199
|
+
return {
|
|
200
|
+
intent: state.intent.id,
|
|
201
|
+
delivery: state.slug,
|
|
202
|
+
...(phase ? { phase } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function publishDeliveryLifecycle(paths, state, eventName, input = {}) {
|
|
207
|
+
let personas = state.intent.personas;
|
|
208
|
+
if (!Array.isArray(personas)) {
|
|
209
|
+
try {
|
|
210
|
+
personas = readRuntimeIntent(paths.projectRoot, state.intent.id)?.personas ?? [];
|
|
211
|
+
} catch {
|
|
212
|
+
personas = [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return publishLifecycleEventSafely(paths.projectRoot, eventName, {
|
|
216
|
+
...input,
|
|
217
|
+
scope: input.scope ?? lifecycleScope(state, input.phase),
|
|
218
|
+
personas,
|
|
219
|
+
streamId: `delivery:${state.slug}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function syncIntentCopies(paths, state) {
|
|
224
|
+
const status = persistedIntentStatus(state);
|
|
225
|
+
const deliveryStatePath = relative(paths.projectRoot, paths.statePath).replaceAll('\\', '/');
|
|
226
|
+
const updated = updateIntentDeliveryState(
|
|
227
|
+
paths.projectRoot,
|
|
228
|
+
resolve(paths.projectRoot, state.intent.path),
|
|
229
|
+
{
|
|
230
|
+
status,
|
|
231
|
+
deliveryStatus: state.status,
|
|
232
|
+
currentPhase: state.currentPhase,
|
|
233
|
+
deliveryStatePath
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
state.intent.status = status;
|
|
237
|
+
return updated;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function fileSnapshot(path) {
|
|
241
|
+
return { path, existed: existsSync(path), content: existsSync(path) ? readFileSync(path, 'utf8') : '' };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function transitionSnapshot(paths, state) {
|
|
245
|
+
const intentPath = resolve(paths.projectRoot, state.intent.path);
|
|
246
|
+
return {
|
|
247
|
+
files: [
|
|
248
|
+
intentPath,
|
|
249
|
+
intentPath.replace(/\.md$/, '.json'),
|
|
250
|
+
paths.statePath,
|
|
251
|
+
paths.trackerPath,
|
|
252
|
+
paths.contextPath
|
|
253
|
+
].map(fileSnapshot),
|
|
254
|
+
operational: readWorkItemView(paths.projectRoot, state.intent.id)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function restoreTransition(paths, snapshot) {
|
|
259
|
+
for (const file of snapshot.files) {
|
|
260
|
+
if (file.existed) atomicText(file.path, file.content);
|
|
261
|
+
else rmSync(file.path, { force: true });
|
|
262
|
+
}
|
|
263
|
+
const before = snapshot.operational;
|
|
264
|
+
if (!before?.item) return;
|
|
265
|
+
updateWorkItem(paths.projectRoot, before.item.id, {
|
|
266
|
+
lane: before.item.lane,
|
|
267
|
+
state: before.item.state,
|
|
268
|
+
intentState: before.item.intentState,
|
|
269
|
+
completionPercent: before.item.completionPercent,
|
|
270
|
+
priority: before.item.priority,
|
|
271
|
+
currentSprint: before.item.currentSprint,
|
|
272
|
+
currentPhase: before.item.currentPhase,
|
|
273
|
+
blockedBy: before.item.blockedBy,
|
|
274
|
+
notes: before.item.notes
|
|
275
|
+
});
|
|
276
|
+
for (const phase of before.phases) {
|
|
277
|
+
setPhase(paths.projectRoot, before.item.id, phase.phaseKey, {
|
|
278
|
+
status: phase.status,
|
|
279
|
+
startedAt: phase.startedAt,
|
|
280
|
+
completedAt: phase.completedAt,
|
|
281
|
+
artefactPath: phase.artefactPath,
|
|
282
|
+
iterationCount: phase.iterationCount,
|
|
283
|
+
notes: phase.notes
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
updateWorkItem(paths.projectRoot, before.item.id, { currentPhase: before.item.currentPhase });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function persistTransition(paths, state) {
|
|
290
|
+
const snapshot = transitionSnapshot(paths, state);
|
|
291
|
+
try {
|
|
292
|
+
state.updatedAt = now();
|
|
293
|
+
const copies = syncIntentCopies(paths, state);
|
|
294
|
+
writeDeliveryDocuments(paths, state);
|
|
295
|
+
const item = syncOperationalProjection(paths, state);
|
|
296
|
+
validateIntentStateCopies(paths.projectRoot, copies.markdownPath, {
|
|
297
|
+
status: state.intent.status,
|
|
298
|
+
deliveryStatus: state.status,
|
|
299
|
+
currentPhase: state.currentPhase,
|
|
300
|
+
deliveryStatePath: relative(paths.projectRoot, paths.statePath).replaceAll('\\', '/')
|
|
301
|
+
});
|
|
302
|
+
if (item.currentPhase !== state.currentPhase || item.intentState !== state.intent.status || item.state !== state.status) {
|
|
303
|
+
throw new Error('Intent state drift: SQLite does not match the Markdown, JSON, and durable delivery state.');
|
|
304
|
+
}
|
|
305
|
+
return item;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
try {
|
|
308
|
+
restoreTransition(paths, snapshot);
|
|
309
|
+
} catch (rollbackError) {
|
|
310
|
+
error.rollbackError = rollbackError.message;
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function assertIntentCopiesConsistent(paths, state) {
|
|
317
|
+
return validateIntentStateCopies(paths.projectRoot, resolve(paths.projectRoot, state.intent.path), {
|
|
318
|
+
status: state.intent.status,
|
|
319
|
+
deliveryStatus: state.status,
|
|
320
|
+
currentPhase: state.currentPhase,
|
|
321
|
+
deliveryStatePath: relative(paths.projectRoot, paths.statePath).replaceAll('\\', '/')
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validatePlanFingerprint(paths) {
|
|
326
|
+
const path = resolve(paths.deliveryRoot, 'plan-fingerprint.json');
|
|
327
|
+
if (!existsSync(path)) throw new Error('Shelf-ready delivery requires plan-fingerprint.json before pausing.');
|
|
328
|
+
const fingerprint = JSON.parse(readFileSync(path, 'utf8'));
|
|
329
|
+
for (const key of ['captured_at', 'captured_by_phase', 'run_mode', 'branch_strategy', 'repos', 'files']) {
|
|
330
|
+
if (fingerprint[key] === undefined || fingerprint[key] === null) {
|
|
331
|
+
throw new Error(`Plan fingerprint is missing required field: ${key}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (fingerprint.run_mode !== 'shelf') throw new Error('Plan fingerprint run_mode must be shelf.');
|
|
335
|
+
if (!Array.isArray(fingerprint.files) || typeof fingerprint.repos !== 'object') {
|
|
336
|
+
throw new Error('Plan fingerprint repos/files have invalid types.');
|
|
337
|
+
}
|
|
338
|
+
return { path, fingerprint };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function finishCurrentRun(paths, state, status, summary) {
|
|
342
|
+
const runId = state.runs.at(-1);
|
|
343
|
+
if (!runId) return null;
|
|
344
|
+
const runPath = resolve(paths.runsRoot, `${runId}.json`);
|
|
345
|
+
if (!existsSync(runPath)) throw new Error(`Durable delivery run record is missing: ${relative(paths.projectRoot, runPath)}`);
|
|
346
|
+
const run = JSON.parse(readFileSync(runPath, 'utf8'));
|
|
347
|
+
run.status = status;
|
|
348
|
+
run.completedAt = now();
|
|
349
|
+
run.summary = summary;
|
|
350
|
+
atomicJson(runPath, run);
|
|
351
|
+
finishCommandRun(paths.projectRoot, runId, {
|
|
352
|
+
status,
|
|
353
|
+
completedAt: run.completedAt,
|
|
354
|
+
exitCode: status === 'failed' ? 1 : 0,
|
|
355
|
+
summary
|
|
356
|
+
});
|
|
357
|
+
finishActiveSession(paths.projectRoot, state.intent.id, {
|
|
358
|
+
ownerId: run.id,
|
|
359
|
+
tool: run.tool,
|
|
360
|
+
phaseKey: run.phase,
|
|
361
|
+
status: status === 'failed' ? 'failed' : 'completed',
|
|
362
|
+
summary,
|
|
363
|
+
});
|
|
364
|
+
return run;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function legacyDeliveryOrchestrator(paths, state) {
|
|
368
|
+
const runId = state.runs?.at(-1);
|
|
369
|
+
if (!runId) return 'manual';
|
|
370
|
+
const runPath = resolve(paths.runsRoot, `${runId}.json`);
|
|
371
|
+
if (!existsSync(runPath)) return 'manual';
|
|
372
|
+
try {
|
|
373
|
+
return JSON.parse(readFileSync(runPath, 'utf8')).tool ?? 'manual';
|
|
374
|
+
} catch {
|
|
375
|
+
return 'manual';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function normaliseDeliveryValidation(paths, state) {
|
|
380
|
+
if (state.validation) {
|
|
381
|
+
state.validation.orchestrator = canonicalValidationProvider(state.validation.orchestrator) || 'manual';
|
|
382
|
+
const independentProvider = (provider) => {
|
|
383
|
+
const canonical = canonicalValidationProvider(provider);
|
|
384
|
+
return canonical && canonical !== state.validation.orchestrator;
|
|
385
|
+
};
|
|
386
|
+
if (state.validation.providers?.gemini && !state.validation.providers.antigravity) {
|
|
387
|
+
state.validation.providers.antigravity = state.validation.providers.gemini;
|
|
388
|
+
}
|
|
389
|
+
if (state.validation.providers) delete state.validation.providers.gemini;
|
|
390
|
+
for (const checkpoint of Object.values(state.validation.checkpoints ?? {})) {
|
|
391
|
+
checkpoint.validators = [...new Set(
|
|
392
|
+
(checkpoint.validators ?? [])
|
|
393
|
+
.map(canonicalValidationProvider)
|
|
394
|
+
.filter(independentProvider),
|
|
395
|
+
)];
|
|
396
|
+
}
|
|
397
|
+
state.providers = [...new Set(
|
|
398
|
+
(state.providers ?? [])
|
|
399
|
+
.map(canonicalValidationProvider)
|
|
400
|
+
.filter(independentProvider),
|
|
401
|
+
)];
|
|
402
|
+
for (const phase of state.phases) {
|
|
403
|
+
if (phase.validation) {
|
|
404
|
+
phase.validation.providers = [...new Set(
|
|
405
|
+
(phase.validation.providers ?? [])
|
|
406
|
+
.map(canonicalValidationProvider)
|
|
407
|
+
.filter(independentProvider),
|
|
408
|
+
)];
|
|
409
|
+
phase.validation.cycles ??= [];
|
|
410
|
+
if (phase.status === 'pending' && phase.validation.enabled && !phase.validation.providers.length) {
|
|
411
|
+
phase.status = 'not-supported';
|
|
412
|
+
phase.statusReason = 'No independent external validator remains after provider migration.';
|
|
413
|
+
phase.completedAt = now();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return state;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const { config } = loadProjectConfig(paths.projectRoot);
|
|
421
|
+
const orchestrator = legacyDeliveryOrchestrator(paths, state);
|
|
422
|
+
const validation = effectiveValidationConfig(config, orchestrator);
|
|
423
|
+
const legacyProviders = [...new Set(
|
|
424
|
+
(Array.isArray(state.providers) ? state.providers : [])
|
|
425
|
+
.map(canonicalValidationProvider)
|
|
426
|
+
.filter((provider) => VALIDATION_PROVIDERS.includes(provider)),
|
|
427
|
+
)];
|
|
428
|
+
|
|
429
|
+
for (const [checkpointName, checkpoint] of Object.entries(validation.checkpoints)) {
|
|
430
|
+
const configured = config.validation.external.checkpoints[checkpointName];
|
|
431
|
+
const configuredValidators = configured.validators === 'auto'
|
|
432
|
+
? 'auto'
|
|
433
|
+
: [...new Set(configured.validators.map(canonicalValidationProvider))];
|
|
434
|
+
const requested = configuredValidators === 'auto'
|
|
435
|
+
? legacyProviders
|
|
436
|
+
: configuredValidators.filter((provider) => legacyProviders.includes(provider));
|
|
437
|
+
checkpoint.validators = configured.enabled
|
|
438
|
+
? requested.filter((provider) => provider !== validation.orchestrator)
|
|
439
|
+
: [];
|
|
440
|
+
}
|
|
441
|
+
validation.migratedFromLegacyState = true;
|
|
442
|
+
state.validation = validation;
|
|
443
|
+
state.providers = [...new Set(
|
|
444
|
+
Object.values(validation.checkpoints).flatMap((checkpoint) => checkpoint.validators),
|
|
445
|
+
)];
|
|
446
|
+
|
|
447
|
+
for (const phase of state.phases) {
|
|
448
|
+
const checkpointName = validationCheckpointForPhase(phase.id);
|
|
449
|
+
if (!checkpointName) {
|
|
450
|
+
phase.validation ??= null;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const checkpoint = validation.checkpoints[checkpointName];
|
|
454
|
+
phase.validation = {
|
|
455
|
+
checkpoint: checkpointName,
|
|
456
|
+
enabled: checkpoint.enabled,
|
|
457
|
+
maxCycles: checkpoint.max_cycles,
|
|
458
|
+
providers: [...checkpoint.validators],
|
|
459
|
+
breadth: checkpoint.review.breadth,
|
|
460
|
+
depth: checkpoint.review.depth,
|
|
461
|
+
output: checkpoint.review.output,
|
|
462
|
+
cycles: [],
|
|
463
|
+
};
|
|
464
|
+
if (phase.status === 'pending' && (!checkpoint.enabled || !checkpoint.validators.length)) {
|
|
465
|
+
phase.status = 'not-supported';
|
|
466
|
+
phase.statusReason = checkpoint.enabled
|
|
467
|
+
? 'No independent external validator remains after legacy-state migration.'
|
|
468
|
+
: 'This external validation checkpoint is disabled in the project configuration.';
|
|
469
|
+
phase.completedAt = now();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return state;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function readDeliveryState(projectRoot, slug) {
|
|
476
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
477
|
+
if (!existsSync(paths.statePath)) throw new Error(`No EWAI delivery state exists for ${slug}`);
|
|
478
|
+
const state = JSON.parse(readFileSync(paths.statePath, 'utf8'));
|
|
479
|
+
if (state.schema !== 'ewai.delivery-state/v1' || state.slug !== slug) {
|
|
480
|
+
throw new Error(`Invalid EWAI delivery state for ${slug}`);
|
|
481
|
+
}
|
|
482
|
+
return normaliseDeliveryValidation(paths, state);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function beginDelivery(projectRoot, slug, options = {}) {
|
|
486
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
487
|
+
if (existsSync(paths.statePath)) throw new Error(`Delivery already exists for ${slug}; continue the existing work instead.`);
|
|
488
|
+
const { config } = loadProjectConfig(paths.projectRoot);
|
|
489
|
+
const intent = readRuntimeIntent(paths.projectRoot, slug);
|
|
490
|
+
if (!intent) throw new Error(`Cannot begin delivery without an intent: ${slug}`);
|
|
491
|
+
const orchestrator = options.tool ?? process.env.EWAI_ORCHESTRATOR ?? 'manual';
|
|
492
|
+
const validation = effectiveValidationConfig(config, orchestrator);
|
|
493
|
+
const providers = [...new Set(
|
|
494
|
+
Object.values(validation.checkpoints).flatMap((checkpoint) => checkpoint.validators),
|
|
495
|
+
)];
|
|
496
|
+
const timestamp = now();
|
|
497
|
+
const phases = deliveryContract.stages.map((stage) => newPhase(stage, options, validation));
|
|
498
|
+
const adjuncts = deliveryContract.adjuncts.map((adjunct) => ({
|
|
499
|
+
id: adjunct.id,
|
|
500
|
+
status: adjunct.id === 'ui-design' && options.ui ? 'pending' : 'skipped',
|
|
501
|
+
statusReason: adjunct.id === 'ui-design' && options.ui
|
|
502
|
+
? ''
|
|
503
|
+
: adjunct.id === 'fit-check' ? 'Not resuming a shelf-ready plan.' : 'No user-facing interface work is in scope.',
|
|
504
|
+
startedAt: null,
|
|
505
|
+
completedAt: adjunct.id === 'ui-design' && options.ui ? null : timestamp,
|
|
506
|
+
gatePath: null,
|
|
507
|
+
gateSha256: null,
|
|
508
|
+
artefactPath: null,
|
|
509
|
+
iterations: 0
|
|
510
|
+
}));
|
|
511
|
+
const firstPhase = phases.find((phase) => phase.id === (options.ideate ? 'ideate' : 'intent'));
|
|
512
|
+
firstPhase.status = 'running';
|
|
513
|
+
firstPhase.startedAt = timestamp;
|
|
514
|
+
const run = {
|
|
515
|
+
schema: 'ewai.delivery-run/v1',
|
|
516
|
+
id: randomUUID(),
|
|
517
|
+
slug,
|
|
518
|
+
tool: orchestrator,
|
|
519
|
+
mode: options.mode ?? 'normal',
|
|
520
|
+
status: 'running',
|
|
521
|
+
phase: firstPhase.id,
|
|
522
|
+
startedAt: timestamp,
|
|
523
|
+
completedAt: null,
|
|
524
|
+
summary: 'Delivery initialized through the EWAI fourteen-stage harness.'
|
|
525
|
+
};
|
|
526
|
+
const state = {
|
|
527
|
+
schema: 'ewai.delivery-state/v1',
|
|
528
|
+
contract: deliveryContract.schema,
|
|
529
|
+
slug,
|
|
530
|
+
intent: {
|
|
531
|
+
id: intent.id,
|
|
532
|
+
slug: intent.slug,
|
|
533
|
+
domain: intent.domain,
|
|
534
|
+
title: intent.title,
|
|
535
|
+
status: intent.status,
|
|
536
|
+
path: relative(paths.projectRoot, intent.path).replaceAll('\\', '/'),
|
|
537
|
+
personas: intent.personas ?? [],
|
|
538
|
+
},
|
|
539
|
+
mode: options.mode ?? 'normal',
|
|
540
|
+
intensity: options.intensity ?? (options.existingCode ? 'scoped' : 'full'),
|
|
541
|
+
status: 'in-progress',
|
|
542
|
+
currentPhase: firstPhase.id,
|
|
543
|
+
startedAt: timestamp,
|
|
544
|
+
updatedAt: timestamp,
|
|
545
|
+
providers,
|
|
546
|
+
validation,
|
|
547
|
+
phases,
|
|
548
|
+
adjuncts,
|
|
549
|
+
humanGates: [{
|
|
550
|
+
id: 'manual-qa',
|
|
551
|
+
status: 'pending',
|
|
552
|
+
startedAt: null,
|
|
553
|
+
completedAt: null,
|
|
554
|
+
approvedBy: null,
|
|
555
|
+
approvedAt: null,
|
|
556
|
+
evidencePath: null
|
|
557
|
+
}],
|
|
558
|
+
approvals: { build: null },
|
|
559
|
+
artefactContracts: { prototype: 'ewai.prototype-manifest/v3' },
|
|
560
|
+
runs: [run.id],
|
|
561
|
+
blockedReason: ''
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
mkdirSync(paths.runsRoot, { recursive: true });
|
|
565
|
+
mkdirSync(paths.gatesRoot, { recursive: true });
|
|
566
|
+
atomicJson(resolve(paths.runsRoot, `${run.id}.json`), run);
|
|
567
|
+
startCommandRun(paths.projectRoot, intent.id, {
|
|
568
|
+
runUuid: run.id,
|
|
569
|
+
phaseKey: run.phase,
|
|
570
|
+
tool: run.tool,
|
|
571
|
+
command: `ewai delivery begin ${slug}`,
|
|
572
|
+
mode: run.mode,
|
|
573
|
+
startedAt: run.startedAt,
|
|
574
|
+
summary: run.summary
|
|
575
|
+
});
|
|
576
|
+
for (const phase of [...phases, ...adjuncts, ...state.humanGates]) {
|
|
577
|
+
setPhase(paths.projectRoot, intent.id, phase.id, {
|
|
578
|
+
status: phase.status,
|
|
579
|
+
startedAt: phase.startedAt,
|
|
580
|
+
completedAt: phase.completedAt,
|
|
581
|
+
notes: phase.statusReason
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
persistTransition(paths, state);
|
|
585
|
+
publishDeliveryLifecycle(paths, state, 'ewai.delivery.phase.entered', {
|
|
586
|
+
sourceKey: `delivery:${state.slug}:phase:${firstPhase.id}:entered:${firstPhase.startedAt}`,
|
|
587
|
+
occurredAt: firstPhase.startedAt,
|
|
588
|
+
sourceRevision: state.updatedAt,
|
|
589
|
+
phase: firstPhase.id,
|
|
590
|
+
facts: { status: 'running', phase: firstPhase.id },
|
|
591
|
+
now: firstPhase.startedAt,
|
|
592
|
+
});
|
|
593
|
+
const active = startActiveSession(paths.projectRoot, intent.id, {
|
|
594
|
+
ownerId: run.id,
|
|
595
|
+
tool: run.tool,
|
|
596
|
+
phaseKey: firstPhase.id,
|
|
597
|
+
summary: `Started EWAI delivery at the ${firstPhase.id} phase.`
|
|
598
|
+
});
|
|
599
|
+
return { state, run, active };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function recordPhaseGate(projectRoot, slug, phaseId, input = {}) {
|
|
603
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
604
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
605
|
+
const phase = findTrackedPhase(state, phaseId);
|
|
606
|
+
return recordPhaseGateAtPaths(paths, slug, phaseId, input, {
|
|
607
|
+
providers: phase.validation?.providers ?? state.providers ?? [],
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export function recordExternalValidationCycle(projectRoot, slug, phaseId, input = {}) {
|
|
612
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
613
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
614
|
+
const phase = findTrackedPhase(state, phaseId);
|
|
615
|
+
if (phase.status !== 'running') throw new Error(`Phase ${phaseId} is not running.`);
|
|
616
|
+
if (!phase.validation) throw new Error(`Phase ${phaseId} is not an external-validation checkpoint.`);
|
|
617
|
+
|
|
618
|
+
const provider = String(input.provider ?? '').trim().toLowerCase();
|
|
619
|
+
if (!phase.validation.providers.includes(provider)) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`${provider || 'Provider'} is not an independent validator selected for ${phase.validation.checkpoint}.`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const outcome = String(input.outcome ?? '').trim().toLowerCase();
|
|
625
|
+
if (!['pass', 'issues'].includes(outcome)) {
|
|
626
|
+
throw new Error('Validation cycle outcome must be pass or issues.');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const prior = phase.validation.cycles.filter((cycle) => cycle.provider === provider);
|
|
630
|
+
if (prior.at(-1)?.outcome === 'pass') {
|
|
631
|
+
throw new Error(`${provider} has already passed ${phaseId}.`);
|
|
632
|
+
}
|
|
633
|
+
const cycle = prior.length + 1;
|
|
634
|
+
if (cycle > phase.validation.maxCycles) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`${provider} has exhausted the configured ${phase.validation.maxCycles} validation cycle(s) for ${phaseId}.`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const responsePath = input.responsePath
|
|
641
|
+
?? `${phaseId}/round-${cycle}-${provider}-response.md`;
|
|
642
|
+
const response = validationEvidencePath(paths, responsePath);
|
|
643
|
+
let fixPath = input.fixPath ?? null;
|
|
644
|
+
let fix = null;
|
|
645
|
+
if (outcome === 'issues') {
|
|
646
|
+
if (!fixPath) {
|
|
647
|
+
throw new Error('A validation cycle with issues requires fix evidence via fixPath.');
|
|
648
|
+
}
|
|
649
|
+
fix = validationEvidencePath(paths, fixPath);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const record = {
|
|
653
|
+
provider,
|
|
654
|
+
cycle,
|
|
655
|
+
outcome,
|
|
656
|
+
breadth: phase.validation.breadth,
|
|
657
|
+
depth: phase.validation.depth,
|
|
658
|
+
output: phase.validation.output,
|
|
659
|
+
responsePath,
|
|
660
|
+
responseSha256: sha256(readFileSync(response)),
|
|
661
|
+
fixPath,
|
|
662
|
+
fixSha256: fix ? sha256(readFileSync(fix)) : null,
|
|
663
|
+
recordedAt: now(),
|
|
664
|
+
notes: String(input.notes ?? '').trim(),
|
|
665
|
+
};
|
|
666
|
+
phase.validation.cycles.push(record);
|
|
667
|
+
phase.iterations = Math.max(
|
|
668
|
+
phase.iterations,
|
|
669
|
+
...phase.validation.providers.map((candidate) => (
|
|
670
|
+
phase.validation.cycles.filter((item) => item.provider === candidate).length
|
|
671
|
+
)),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
const ledgerPath = resolve(paths.deliveryRoot, phaseId, 'validation-cycles.json');
|
|
675
|
+
atomicJson(ledgerPath, {
|
|
676
|
+
schema: 'ewai.external-validation-cycles/v1',
|
|
677
|
+
slug,
|
|
678
|
+
phase: phaseId,
|
|
679
|
+
checkpoint: phase.validation.checkpoint,
|
|
680
|
+
orchestrator: state.validation?.orchestrator ?? 'manual',
|
|
681
|
+
maxCycles: phase.validation.maxCycles,
|
|
682
|
+
breadth: phase.validation.breadth,
|
|
683
|
+
depth: phase.validation.depth,
|
|
684
|
+
output: phase.validation.output,
|
|
685
|
+
providers: phase.validation.providers,
|
|
686
|
+
cycles: phase.validation.cycles,
|
|
687
|
+
});
|
|
688
|
+
persistTransition(paths, state);
|
|
689
|
+
addActivityEvent(paths.projectRoot, state.intent.id, {
|
|
690
|
+
eventType: 'progress',
|
|
691
|
+
phaseKey: phaseId,
|
|
692
|
+
summary: `${provider} validation cycle ${cycle}/${phase.validation.maxCycles}: ${outcome}.`,
|
|
693
|
+
details: responsePath,
|
|
694
|
+
});
|
|
695
|
+
return { state, record, ledgerPath };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function phaseGateTemplate(projectRoot, slug, phaseId) {
|
|
699
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
700
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
701
|
+
const phase = findTrackedPhase(state, phaseId);
|
|
702
|
+
return phaseGateTemplateAtPaths(paths, slug, phaseId, {
|
|
703
|
+
providers: phase.validation?.providers ?? state.providers ?? [],
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function continueDelivery(projectRoot, slug) {
|
|
708
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
709
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
710
|
+
assertIntentCopiesConsistent(paths, state);
|
|
711
|
+
assertCompletedEvidenceFresh(paths, state);
|
|
712
|
+
if (state.status === 'shelf-ready') {
|
|
713
|
+
validatePlanFingerprint(paths);
|
|
714
|
+
return {
|
|
715
|
+
slug,
|
|
716
|
+
status: state.status,
|
|
717
|
+
currentPhase: state.currentPhase,
|
|
718
|
+
nextPhase: 'fit-check',
|
|
719
|
+
requiresResume: true,
|
|
720
|
+
command: `ewai delivery resume ${slug}`,
|
|
721
|
+
statePath: relative(paths.projectRoot, paths.statePath).replaceAll('\\', '/')
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (state.status === 'validated-not-for-build') {
|
|
725
|
+
return {
|
|
726
|
+
slug,
|
|
727
|
+
status: state.status,
|
|
728
|
+
currentPhase: state.currentPhase,
|
|
729
|
+
nextPhase: null,
|
|
730
|
+
requiresResume: false,
|
|
731
|
+
command: null,
|
|
732
|
+
statePath: relative(paths.projectRoot, paths.statePath).replaceAll('\\', '/')
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const nextPhase = nextRequiredPhase(state);
|
|
736
|
+
return {
|
|
737
|
+
slug,
|
|
738
|
+
status: state.status,
|
|
739
|
+
currentPhase: state.currentPhase,
|
|
740
|
+
nextPhase,
|
|
741
|
+
command: `ewai delivery continue ${slug}`,
|
|
742
|
+
statePath: relative(paths.projectRoot, paths.statePath).replaceAll('\\', '/')
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function startDeliveryPhase(projectRoot, slug, phaseId) {
|
|
747
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
748
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
749
|
+
assertIntentCopiesConsistent(paths, state);
|
|
750
|
+
assertCompletedEvidenceFresh(paths, state);
|
|
751
|
+
if (state.status === 'shelf-ready') throw new Error('Shelf-ready work must resume through FitCheck before Build.');
|
|
752
|
+
if (state.status === 'validated-not-for-build') throw new Error('A dry-run is validated but explicitly not approved for Build.');
|
|
753
|
+
const expected = nextRequiredPhase(state);
|
|
754
|
+
if (expected !== phaseId) throw new Error(`Cannot start ${phaseId}; the next required phase is ${expected}.`);
|
|
755
|
+
const phase = findTrackedPhase(state, phaseId);
|
|
756
|
+
if (phaseId === 'build') {
|
|
757
|
+
const approvalPath = resolve(paths.gatesRoot, 'build/build-approval.json');
|
|
758
|
+
if (!state.approvals.build || !existsSync(approvalPath)) {
|
|
759
|
+
throw new Error('Build requires explicit Build approval recorded in SPECS before code or branch creation.');
|
|
760
|
+
}
|
|
761
|
+
const approval = JSON.parse(readFileSync(approvalPath, 'utf8'));
|
|
762
|
+
if (approval.schema !== 'ewai.build-approval/v1' || approval.decision !== 'approved') {
|
|
763
|
+
throw new Error('Build requires explicit Build approval recorded in SPECS before code or branch creation.');
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (phaseId === 'retro') {
|
|
767
|
+
const manualQa = state.humanGates.find((gate) => gate.id === 'manual-qa');
|
|
768
|
+
if (manualQa.status !== 'approved') throw new Error('Retro cannot start until Manual QA is explicitly approved.');
|
|
769
|
+
}
|
|
770
|
+
phase.status = 'running';
|
|
771
|
+
phase.startedAt ??= now();
|
|
772
|
+
state.currentPhase = phaseId;
|
|
773
|
+
persistTransition(paths, state);
|
|
774
|
+
publishDeliveryLifecycle(paths, state, 'ewai.delivery.phase.entered', {
|
|
775
|
+
sourceKey: `delivery:${state.slug}:phase:${phaseId}:entered:${phase.startedAt}`,
|
|
776
|
+
occurredAt: phase.startedAt,
|
|
777
|
+
sourceRevision: state.updatedAt,
|
|
778
|
+
phase: phaseId,
|
|
779
|
+
facts: { status: 'running', phase: phaseId },
|
|
780
|
+
now: phase.startedAt,
|
|
781
|
+
});
|
|
782
|
+
addActivityEvent(paths.projectRoot, state.intent.id, {
|
|
783
|
+
eventType: 'handoff',
|
|
784
|
+
phaseKey: phaseId,
|
|
785
|
+
summary: `Entered EWAI phase: ${phaseId}.`
|
|
786
|
+
});
|
|
787
|
+
return state;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function completeDeliveryPhase(projectRoot, slug, phaseId, options = {}) {
|
|
791
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
792
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
793
|
+
const phase = findTrackedPhase(state, phaseId);
|
|
794
|
+
if (phase.status !== 'running') throw new Error(`Phase ${phaseId} is not running.`);
|
|
795
|
+
if (phaseId === 'intent') {
|
|
796
|
+
const intent = readRuntimeIntent(paths.projectRoot, state.intent.id);
|
|
797
|
+
if (!intent || !acceptedIntentStatuses.has(String(intent.status).toLowerCase())) {
|
|
798
|
+
throw new Error('Intent cannot complete until its project-owned status is ready or approved.');
|
|
799
|
+
}
|
|
800
|
+
state.intent.status = intent.status;
|
|
801
|
+
}
|
|
802
|
+
assertIntentCopiesConsistent(paths, state);
|
|
803
|
+
const phaseProviders = phase.validation?.providers ?? state.providers;
|
|
804
|
+
if (phase.validation) assertExternalValidationComplete(phase);
|
|
805
|
+
validatePhaseArtefacts(
|
|
806
|
+
paths.deliveryRoot,
|
|
807
|
+
phaseId,
|
|
808
|
+
phaseProviders,
|
|
809
|
+
phase.validation?.cycles ?? [],
|
|
810
|
+
);
|
|
811
|
+
const validated = readPassingGate(paths, phaseId, { providers: phaseProviders });
|
|
812
|
+
phase.status = 'completed';
|
|
813
|
+
phase.completedAt = now();
|
|
814
|
+
phase.gatePath = relative(paths.projectRoot, validated.path).replaceAll('\\', '/');
|
|
815
|
+
phase.gateSha256 = validated.sha256;
|
|
816
|
+
phase.artefactPath = options.artefactPath
|
|
817
|
+
?? validated.gate.required_gates.find((gate) => gate.status === 'pass')?.output_path
|
|
818
|
+
?? null;
|
|
819
|
+
state.currentPhase = nextRequiredPhase(state);
|
|
820
|
+
if (state.currentPhase === 'build' && state.mode === 'shelf') {
|
|
821
|
+
validatePlanFingerprint(paths);
|
|
822
|
+
state.currentPhase = 'shelf-ready';
|
|
823
|
+
state.status = 'shelf-ready';
|
|
824
|
+
}
|
|
825
|
+
if (state.currentPhase === 'build' && state.mode === 'dry-run') {
|
|
826
|
+
state.currentPhase = 'complete-dry-run';
|
|
827
|
+
state.status = 'validated-not-for-build';
|
|
828
|
+
}
|
|
829
|
+
if (state.currentPhase === 'manual-qa') {
|
|
830
|
+
const manualQa = state.humanGates.find((gate) => gate.id === 'manual-qa');
|
|
831
|
+
manualQa.status = 'running';
|
|
832
|
+
manualQa.startedAt ??= now();
|
|
833
|
+
state.status = 'paused-awaiting-manual-qa';
|
|
834
|
+
}
|
|
835
|
+
if (state.currentPhase === 'complete') state.status = 'completed';
|
|
836
|
+
persistTransition(paths, state);
|
|
837
|
+
publishDeliveryLifecycle(paths, state, 'ewai.delivery.phase.completed', {
|
|
838
|
+
sourceKey: `delivery:${state.slug}:phase:${phaseId}:completed:${phase.completedAt}`,
|
|
839
|
+
occurredAt: phase.completedAt,
|
|
840
|
+
sourceRevision: state.updatedAt,
|
|
841
|
+
phase: phaseId,
|
|
842
|
+
facts: { status: 'completed', phase: phaseId },
|
|
843
|
+
evidence: phase.gatePath ? [phase.gatePath] : [],
|
|
844
|
+
now: phase.completedAt,
|
|
845
|
+
});
|
|
846
|
+
if (phaseId === 'delivery') publishDeliveryLifecycle(paths, state, 'ewai.delivery.completed', {
|
|
847
|
+
sourceKey: `delivery:${state.slug}:completed:${phase.completedAt}`,
|
|
848
|
+
occurredAt: phase.completedAt,
|
|
849
|
+
sourceRevision: state.updatedAt,
|
|
850
|
+
facts: { status: 'completed' },
|
|
851
|
+
evidence: phase.gatePath ? [phase.gatePath] : [],
|
|
852
|
+
now: phase.completedAt,
|
|
853
|
+
});
|
|
854
|
+
addActivityEvent(paths.projectRoot, state.intent.id, {
|
|
855
|
+
eventType: 'progress',
|
|
856
|
+
phaseKey: phaseId,
|
|
857
|
+
summary: `Completed EWAI phase: ${phaseId}.`,
|
|
858
|
+
details: phase.artefactPath ?? ''
|
|
859
|
+
});
|
|
860
|
+
if (state.status === 'shelf-ready') {
|
|
861
|
+
finishCurrentRun(paths, state, 'shelf-ready', 'Shelf package complete and awaiting Build resume.');
|
|
862
|
+
} else if (state.status === 'validated-not-for-build') {
|
|
863
|
+
finishCurrentRun(paths, state, 'completed', 'Dry-run validation complete; not approved for Build.');
|
|
864
|
+
} else if (state.status === 'completed') {
|
|
865
|
+
finishCurrentRun(paths, state, 'completed', 'EWAI delivery and Retro complete.');
|
|
866
|
+
}
|
|
867
|
+
return state;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export function resumeShelvedDelivery(projectRoot, slug, input = {}) {
|
|
871
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
872
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
873
|
+
assertIntentCopiesConsistent(paths, state);
|
|
874
|
+
assertCompletedEvidenceFresh(paths, state);
|
|
875
|
+
validatePlanFingerprint(paths);
|
|
876
|
+
if (state.status !== 'shelf-ready') throw new Error(`Delivery ${slug} is not shelf-ready.`);
|
|
877
|
+
const fitCheck = state.adjuncts.find((phase) => phase.id === 'fit-check');
|
|
878
|
+
fitCheck.status = 'pending';
|
|
879
|
+
fitCheck.statusReason = '';
|
|
880
|
+
fitCheck.startedAt = null;
|
|
881
|
+
fitCheck.completedAt = null;
|
|
882
|
+
state.mode = 'resume';
|
|
883
|
+
state.status = 'in-progress';
|
|
884
|
+
state.currentPhase = 'fit-check';
|
|
885
|
+
const run = {
|
|
886
|
+
schema: 'ewai.delivery-run/v1',
|
|
887
|
+
id: randomUUID(),
|
|
888
|
+
slug,
|
|
889
|
+
tool: input.tool ?? 'manual',
|
|
890
|
+
mode: 'resume',
|
|
891
|
+
status: 'running',
|
|
892
|
+
phase: 'fit-check',
|
|
893
|
+
startedAt: now(),
|
|
894
|
+
completedAt: null,
|
|
895
|
+
summary: 'Shelf-ready delivery resumed through mandatory FitCheck.'
|
|
896
|
+
};
|
|
897
|
+
atomicJson(resolve(paths.runsRoot, `${run.id}.json`), run);
|
|
898
|
+
startCommandRun(paths.projectRoot, state.intent.id, {
|
|
899
|
+
runUuid: run.id,
|
|
900
|
+
phaseKey: run.phase,
|
|
901
|
+
tool: run.tool,
|
|
902
|
+
command: `ewai delivery resume ${slug}`,
|
|
903
|
+
mode: run.mode,
|
|
904
|
+
startedAt: run.startedAt,
|
|
905
|
+
summary: run.summary
|
|
906
|
+
});
|
|
907
|
+
state.runs.push(run.id);
|
|
908
|
+
persistTransition(paths, state);
|
|
909
|
+
const active = startActiveSession(paths.projectRoot, state.intent.id, {
|
|
910
|
+
ownerId: run.id,
|
|
911
|
+
tool: run.tool,
|
|
912
|
+
phaseKey: 'fit-check',
|
|
913
|
+
summary: 'Resumed shelf-ready EWAI delivery at FitCheck.'
|
|
914
|
+
});
|
|
915
|
+
return { state, run, active };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export function ratifyDeliveryAmendments(projectRoot, slug, input = {}) {
|
|
919
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
920
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
921
|
+
assertIntentCopiesConsistent(paths, state);
|
|
922
|
+
if (state.status !== 'in-progress' || state.currentPhase !== 'build' || nextRequiredPhase(state) !== 'build') {
|
|
923
|
+
throw new Error('Pre-Build amendments can be ratified only after all planning phases are complete and before Build starts.');
|
|
924
|
+
}
|
|
925
|
+
if (state.approvals.build) throw new Error('Pre-Build amendments must be ratified before Build approval is recorded.');
|
|
926
|
+
if (input.decision !== 'approved') throw new Error('Amendment ratification decision must be approved.');
|
|
927
|
+
if (!input.approvedBy) throw new Error('Amendment ratification requires the approving person or role.');
|
|
928
|
+
if (!String(input.scope ?? '').trim()) throw new Error('Amendment ratification requires the approved amendment scope.');
|
|
929
|
+
|
|
930
|
+
const validatedPhases = [];
|
|
931
|
+
for (const phase of [...state.phases, ...state.adjuncts]) {
|
|
932
|
+
if (phase.status !== 'completed') continue;
|
|
933
|
+
const providers = phase.validation?.providers ?? state.providers ?? [];
|
|
934
|
+
if (phase.validation) assertExternalValidationComplete(phase);
|
|
935
|
+
validatePhaseArtefacts(paths.deliveryRoot, phase.id, providers, phase.validation?.cycles ?? []);
|
|
936
|
+
const validated = readPassingGate(paths, phase.id, { providers });
|
|
937
|
+
validatedPhases.push({
|
|
938
|
+
phase: phase.id,
|
|
939
|
+
previousGateSha256: phase.gateSha256,
|
|
940
|
+
ratifiedGateSha256: validated.sha256,
|
|
941
|
+
gatePath: relative(paths.projectRoot, validated.path).replaceAll('\\', '/'),
|
|
942
|
+
tracked: phase,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const changes = validatedPhases.filter((phase) => phase.previousGateSha256 !== phase.ratifiedGateSha256);
|
|
947
|
+
if (!changes.length) throw new Error('No changed completed-phase gate evidence requires ratification.');
|
|
948
|
+
|
|
949
|
+
const approvedAt = now();
|
|
950
|
+
const ratification = {
|
|
951
|
+
schema: 'ewai.delivery-amendment-ratification/v1',
|
|
952
|
+
slug,
|
|
953
|
+
decision: 'approved',
|
|
954
|
+
approvedBy: input.approvedBy,
|
|
955
|
+
approvedAt,
|
|
956
|
+
scope: String(input.scope).trim(),
|
|
957
|
+
stateSha256BeforeRatification: sha256(readFileSync(paths.statePath)),
|
|
958
|
+
changes: changes.map(({ tracked, ...change }) => change),
|
|
959
|
+
validatedPhases: validatedPhases.map(({ tracked, ...phase }) => ({
|
|
960
|
+
phase: phase.phase,
|
|
961
|
+
gatePath: phase.gatePath,
|
|
962
|
+
gateSha256: phase.ratifiedGateSha256,
|
|
963
|
+
})),
|
|
964
|
+
};
|
|
965
|
+
const ratificationPath = resolve(paths.gatesRoot, 'build/pre-build-amendment-ratification.json');
|
|
966
|
+
const ratificationBefore = fileSnapshot(ratificationPath);
|
|
967
|
+
|
|
968
|
+
for (const phase of validatedPhases) {
|
|
969
|
+
phase.tracked.gatePath = phase.gatePath;
|
|
970
|
+
phase.tracked.gateSha256 = phase.ratifiedGateSha256;
|
|
971
|
+
}
|
|
972
|
+
state.amendments = [
|
|
973
|
+
...(Array.isArray(state.amendments) ? state.amendments : []),
|
|
974
|
+
{
|
|
975
|
+
path: relative(paths.projectRoot, ratificationPath).replaceAll('\\', '/'),
|
|
976
|
+
approvedBy: ratification.approvedBy,
|
|
977
|
+
approvedAt,
|
|
978
|
+
scope: ratification.scope,
|
|
979
|
+
phases: changes.map((change) => change.phase),
|
|
980
|
+
},
|
|
981
|
+
];
|
|
982
|
+
|
|
983
|
+
atomicJson(ratificationPath, ratification);
|
|
984
|
+
try {
|
|
985
|
+
persistTransition(paths, state);
|
|
986
|
+
} catch (error) {
|
|
987
|
+
if (ratificationBefore.existed) atomicText(ratificationPath, ratificationBefore.content);
|
|
988
|
+
else rmSync(ratificationPath, { force: true });
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
addActivityEvent(paths.projectRoot, state.intent.id, {
|
|
992
|
+
eventType: 'decision',
|
|
993
|
+
phaseKey: 'build',
|
|
994
|
+
summary: `Pre-Build amendments ratified by ${ratification.approvedBy}.`,
|
|
995
|
+
details: ratification.scope,
|
|
996
|
+
});
|
|
997
|
+
return ratification;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function completedEvidenceAmendmentReceiptId(slug, expectedStateDigest, approvedBy, reason) {
|
|
1001
|
+
return sha256(Buffer.from(JSON.stringify({
|
|
1002
|
+
schema: 'ewai.completed-evidence-amendment-authority/v1',
|
|
1003
|
+
slug,
|
|
1004
|
+
expectedStateDigest,
|
|
1005
|
+
approvedBy,
|
|
1006
|
+
reason,
|
|
1007
|
+
})));
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function completedEvidenceAmendmentRoot(paths, { create = false } = {}) {
|
|
1011
|
+
const root = resolve(paths.deliveryRoot, 'evidence-amendments');
|
|
1012
|
+
if (existsSync(root)) {
|
|
1013
|
+
const details = lstatSync(root);
|
|
1014
|
+
if (details.isSymbolicLink() || !details.isDirectory()) {
|
|
1015
|
+
throw new Error('Completed evidence amendment storage must be a real directory inside the delivery root.');
|
|
1016
|
+
}
|
|
1017
|
+
} else if (create) {
|
|
1018
|
+
mkdirSync(root);
|
|
1019
|
+
}
|
|
1020
|
+
return root;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function completedEvidenceAmendmentReceiptPath(paths, receiptId) {
|
|
1024
|
+
const root = completedEvidenceAmendmentRoot(paths);
|
|
1025
|
+
const path = resolve(root, `${receiptId}.json`);
|
|
1026
|
+
if (!isWithin(root, path)) throw new Error('Unsafe completed evidence amendment receipt path.');
|
|
1027
|
+
return path;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function deliveryProcessIsActive(pid) {
|
|
1031
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
1032
|
+
try {
|
|
1033
|
+
process.kill(pid, 0);
|
|
1034
|
+
return true;
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
return error?.code === 'EPERM';
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function acquireCompletedEvidenceAmendmentLease(paths) {
|
|
1041
|
+
const leasePath = resolve(paths.deliveryRoot, '.evidence-amendment.lock');
|
|
1042
|
+
const metadataPath = resolve(leasePath, 'lease.json');
|
|
1043
|
+
const leaseId = randomUUID();
|
|
1044
|
+
const acquiredAt = now();
|
|
1045
|
+
|
|
1046
|
+
const create = () => {
|
|
1047
|
+
try {
|
|
1048
|
+
mkdirSync(leasePath);
|
|
1049
|
+
atomicJson(metadataPath, {
|
|
1050
|
+
schema: 'ewai.completed-evidence-amendment-lease/v1',
|
|
1051
|
+
leaseId,
|
|
1052
|
+
pid: process.pid,
|
|
1053
|
+
acquiredAt,
|
|
1054
|
+
});
|
|
1055
|
+
return true;
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if (error?.code === 'EEXIST') return false;
|
|
1058
|
+
rmSync(leasePath, { recursive: true, force: true });
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
if (!create()) {
|
|
1064
|
+
let current = null;
|
|
1065
|
+
try { current = JSON.parse(readFileSync(metadataPath, 'utf8')); } catch { /* malformed metadata is handled by age */ }
|
|
1066
|
+
const valid = current?.schema === 'ewai.completed-evidence-amendment-lease/v1';
|
|
1067
|
+
const age = Date.now() - lstatSync(leasePath).mtimeMs;
|
|
1068
|
+
const stale = valid ? !deliveryProcessIsActive(Number(current.pid)) : age > 15 * 60_000;
|
|
1069
|
+
if (!stale) throw new Error('Another completed evidence amendment ratification is in progress. Preview again after it finishes.');
|
|
1070
|
+
rmSync(leasePath, { recursive: true, force: true });
|
|
1071
|
+
if (!create()) throw new Error('Another completed evidence amendment ratification is in progress. Preview again after it finishes.');
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
return () => {
|
|
1075
|
+
let current = null;
|
|
1076
|
+
try { current = JSON.parse(readFileSync(metadataPath, 'utf8')); } catch { /* never remove a lock we cannot identify */ }
|
|
1077
|
+
if (current?.leaseId === leaseId) rmSync(leasePath, { recursive: true, force: true });
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function completedEvidenceAmendmentAssessment(paths, state) {
|
|
1082
|
+
const deliveryStateSha256 = sha256(readFileSync(paths.statePath));
|
|
1083
|
+
const phases = [];
|
|
1084
|
+
|
|
1085
|
+
for (const phase of [...state.phases, ...state.adjuncts]) {
|
|
1086
|
+
if (phase.status !== 'completed') continue;
|
|
1087
|
+
const ledgerPath = resolve(paths.gatesRoot, phase.id, 'gate-ledger.json');
|
|
1088
|
+
if (!existsSync(ledgerPath)) throw new Error(`Completed phase ${phase.id} has no gate ledger.`);
|
|
1089
|
+
const ledgerRaw = readFileSync(ledgerPath);
|
|
1090
|
+
if (!phase.gateSha256 || sha256(ledgerRaw) !== phase.gateSha256) {
|
|
1091
|
+
throw new Error(`Completed phase ${phase.id} gate ledger changed directly after completion.`);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const candidate = structuredClone(JSON.parse(ledgerRaw));
|
|
1095
|
+
const evidenceChanges = [];
|
|
1096
|
+
for (const item of candidate.required_gates ?? []) {
|
|
1097
|
+
if (item.status !== 'pass') continue;
|
|
1098
|
+
const evidencePath = resolve(paths.deliveryRoot, String(item.output_path ?? ''));
|
|
1099
|
+
if (!item.output_path || !isWithin(paths.deliveryRoot, evidencePath)) {
|
|
1100
|
+
throw new Error(`Completed phase ${phase.id} gate ${item.id} has an unsafe evidence path.`);
|
|
1101
|
+
}
|
|
1102
|
+
const absolute = evidenceAbsolute(paths.projectRoot, evidencePath);
|
|
1103
|
+
const currentSha256 = sha256(readFileSync(absolute));
|
|
1104
|
+
if (currentSha256 === item.evidence_hash) continue;
|
|
1105
|
+
evidenceChanges.push({
|
|
1106
|
+
gateId: item.id,
|
|
1107
|
+
path: item.output_path,
|
|
1108
|
+
previousSha256: item.evidence_hash,
|
|
1109
|
+
currentSha256,
|
|
1110
|
+
});
|
|
1111
|
+
item.evidence_hash = currentSha256;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const providers = phase.validation?.providers ?? state.providers ?? [];
|
|
1115
|
+
validatePhaseGateAtPaths(paths, phase.id, candidate, { providers });
|
|
1116
|
+
if (!evidenceChanges.length) continue;
|
|
1117
|
+
const candidateRaw = Buffer.from(`${JSON.stringify(candidate, null, 2)}\n`);
|
|
1118
|
+
phases.push({
|
|
1119
|
+
phase: phase.id,
|
|
1120
|
+
gatePath: relative(paths.deliveryRoot, ledgerPath).replaceAll('\\', '/'),
|
|
1121
|
+
previousGateSha256: phase.gateSha256,
|
|
1122
|
+
newGateSha256: sha256(candidateRaw),
|
|
1123
|
+
evidenceChanges,
|
|
1124
|
+
candidate,
|
|
1125
|
+
candidateRaw,
|
|
1126
|
+
ledgerPath,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const publicPhases = phases.map(({ candidate, candidateRaw, ledgerPath, ...phase }) => phase);
|
|
1131
|
+
const revision = {
|
|
1132
|
+
schema: 'ewai.completed-evidence-amendment-revision/v1',
|
|
1133
|
+
slug: state.slug,
|
|
1134
|
+
deliveryStateSha256,
|
|
1135
|
+
phases: publicPhases,
|
|
1136
|
+
};
|
|
1137
|
+
return {
|
|
1138
|
+
schema: 'ewai.completed-evidence-amendment-preview/v1',
|
|
1139
|
+
slug: state.slug,
|
|
1140
|
+
status: phases.length ? 'changes-found' : 'no-changes',
|
|
1141
|
+
deliveryStateSha256,
|
|
1142
|
+
stateSha256: sha256(Buffer.from(JSON.stringify(revision))),
|
|
1143
|
+
phases: publicPhases,
|
|
1144
|
+
internalPhases: phases,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export function previewCompletedEvidenceAmendment(projectRoot, slug) {
|
|
1149
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
1150
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
1151
|
+
assertIntentCopiesConsistent(paths, state);
|
|
1152
|
+
const { internalPhases, ...preview } = completedEvidenceAmendmentAssessment(paths, state);
|
|
1153
|
+
return preview;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function replayCompletedEvidenceAmendment(paths, state, receiptPath, input) {
|
|
1157
|
+
if (!existsSync(receiptPath)) return null;
|
|
1158
|
+
const details = lstatSync(receiptPath);
|
|
1159
|
+
if (details.isSymbolicLink() || !details.isFile()) {
|
|
1160
|
+
throw new Error('Completed evidence amendment receipt must be a real file.');
|
|
1161
|
+
}
|
|
1162
|
+
const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'));
|
|
1163
|
+
const receiptRelative = relative(paths.deliveryRoot, receiptPath).replaceAll('\\', '/');
|
|
1164
|
+
const recorded = (state.evidenceAmendments ?? []).find((amendment) => amendment.receiptPath === receiptRelative);
|
|
1165
|
+
if (receipt.schema !== 'ewai.completed-evidence-amendment/v1'
|
|
1166
|
+
|| receipt.slug !== state.slug
|
|
1167
|
+
|| receipt.expectedStateDigest !== input.expectedStateDigest
|
|
1168
|
+
|| receipt.approvedBy !== input.approvedBy
|
|
1169
|
+
|| receipt.reason !== input.reason
|
|
1170
|
+
|| !Array.isArray(receipt.changes)
|
|
1171
|
+
|| !receipt.changes.length
|
|
1172
|
+
|| !recorded
|
|
1173
|
+
|| recorded.approvedBy !== receipt.approvedBy
|
|
1174
|
+
|| recorded.reason !== receipt.reason
|
|
1175
|
+
|| recorded.expectedStateDigest !== receipt.expectedStateDigest
|
|
1176
|
+
|| JSON.stringify(recorded.phases) !== JSON.stringify(receipt.changes.map((change) => change.phase))) {
|
|
1177
|
+
throw new Error('Existing completed evidence amendment receipt does not match the supplied authority.');
|
|
1178
|
+
}
|
|
1179
|
+
for (const change of receipt.changes ?? []) {
|
|
1180
|
+
const phase = [...state.phases, ...state.adjuncts].find((candidate) => candidate.id === change.phase);
|
|
1181
|
+
const ledgerPath = resolve(paths.deliveryRoot, change.gatePath);
|
|
1182
|
+
if (!phase || phase.status !== 'completed' || phase.gateSha256 !== change.newGateSha256
|
|
1183
|
+
|| !isWithin(paths.deliveryRoot, ledgerPath) || !existsSync(ledgerPath)
|
|
1184
|
+
|| sha256(readFileSync(ledgerPath)) !== change.newGateSha256) {
|
|
1185
|
+
throw new Error('Completed evidence amendment replay no longer matches current delivery truth.');
|
|
1186
|
+
}
|
|
1187
|
+
validatePhaseGateAtPaths(paths, phase.id, JSON.parse(readFileSync(ledgerPath, 'utf8')), {
|
|
1188
|
+
providers: phase.validation?.providers ?? state.providers ?? [],
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
action: 'replayed',
|
|
1193
|
+
receipt,
|
|
1194
|
+
receiptPath: receiptRelative,
|
|
1195
|
+
state,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
export function ratifyCompletedEvidenceAmendment(projectRoot, slug, input = {}) {
|
|
1200
|
+
if (input.confirmed !== true) {
|
|
1201
|
+
throw new Error('Completed evidence amendment requires explicit confirmation.');
|
|
1202
|
+
}
|
|
1203
|
+
const approvedBy = String(input.approvedBy ?? '').trim();
|
|
1204
|
+
const reason = String(input.reason ?? '').trim();
|
|
1205
|
+
const expectedStateDigest = String(input.expectedStateDigest ?? '').trim();
|
|
1206
|
+
if (!approvedBy) throw new Error('Completed evidence amendment requires the approving person or role.');
|
|
1207
|
+
if (!reason) throw new Error('Completed evidence amendment requires a reason.');
|
|
1208
|
+
if (!/^[a-f0-9]{64}$/.test(expectedStateDigest)) {
|
|
1209
|
+
throw new Error('Completed evidence amendment requires the exact preview state digest.');
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
1213
|
+
const releaseLease = acquireCompletedEvidenceAmendmentLease(paths);
|
|
1214
|
+
try {
|
|
1215
|
+
return ratifyCompletedEvidenceAmendmentUnderLease(projectRoot, slug, paths, input, approvedBy, reason, expectedStateDigest);
|
|
1216
|
+
} finally {
|
|
1217
|
+
releaseLease();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function ratifyCompletedEvidenceAmendmentUnderLease(projectRoot, slug, paths, input, approvedBy, reason, expectedStateDigest) {
|
|
1222
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
1223
|
+
assertIntentCopiesConsistent(paths, state);
|
|
1224
|
+
const receiptId = completedEvidenceAmendmentReceiptId(
|
|
1225
|
+
state.slug,
|
|
1226
|
+
expectedStateDigest,
|
|
1227
|
+
approvedBy,
|
|
1228
|
+
reason,
|
|
1229
|
+
);
|
|
1230
|
+
const receiptPath = completedEvidenceAmendmentReceiptPath(paths, receiptId);
|
|
1231
|
+
const replay = replayCompletedEvidenceAmendment(paths, state, receiptPath, {
|
|
1232
|
+
expectedStateDigest,
|
|
1233
|
+
approvedBy,
|
|
1234
|
+
reason,
|
|
1235
|
+
});
|
|
1236
|
+
if (replay) return replay;
|
|
1237
|
+
|
|
1238
|
+
const assessment = completedEvidenceAmendmentAssessment(paths, state);
|
|
1239
|
+
if (expectedStateDigest !== assessment.stateSha256) {
|
|
1240
|
+
throw new Error('Completed evidence amendment state digest changed after preview; preview again.');
|
|
1241
|
+
}
|
|
1242
|
+
if (!assessment.internalPhases.length) {
|
|
1243
|
+
throw new Error('No changed completed-phase evidence requires ratification.');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const amendmentRoot = resolve(paths.deliveryRoot, 'evidence-amendments');
|
|
1247
|
+
const rootExisted = existsSync(amendmentRoot);
|
|
1248
|
+
completedEvidenceAmendmentRoot(paths, { create: true });
|
|
1249
|
+
const ledgerSnapshots = assessment.internalPhases.map(({ ledgerPath }) => fileSnapshot(ledgerPath));
|
|
1250
|
+
const receiptSnapshot = fileSnapshot(receiptPath);
|
|
1251
|
+
const approvedAt = now();
|
|
1252
|
+
const receipt = {
|
|
1253
|
+
schema: 'ewai.completed-evidence-amendment/v1',
|
|
1254
|
+
slug: state.slug,
|
|
1255
|
+
approvedBy,
|
|
1256
|
+
approvedAt,
|
|
1257
|
+
reason,
|
|
1258
|
+
expectedStateDigest,
|
|
1259
|
+
deliveryStateSha256Before: assessment.deliveryStateSha256,
|
|
1260
|
+
changes: assessment.phases,
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
for (const amendment of assessment.internalPhases) {
|
|
1264
|
+
const phase = [...state.phases, ...state.adjuncts].find((candidate) => candidate.id === amendment.phase);
|
|
1265
|
+
phase.gateSha256 = amendment.newGateSha256;
|
|
1266
|
+
}
|
|
1267
|
+
state.evidenceAmendments = [
|
|
1268
|
+
...(Array.isArray(state.evidenceAmendments) ? state.evidenceAmendments : []),
|
|
1269
|
+
{
|
|
1270
|
+
receiptPath: relative(paths.deliveryRoot, receiptPath).replaceAll('\\', '/'),
|
|
1271
|
+
approvedBy,
|
|
1272
|
+
approvedAt,
|
|
1273
|
+
reason,
|
|
1274
|
+
expectedStateDigest,
|
|
1275
|
+
phases: assessment.phases.map((phase) => phase.phase),
|
|
1276
|
+
},
|
|
1277
|
+
];
|
|
1278
|
+
|
|
1279
|
+
try {
|
|
1280
|
+
for (const amendment of assessment.internalPhases) {
|
|
1281
|
+
atomicText(amendment.ledgerPath, amendment.candidateRaw.toString('utf8'));
|
|
1282
|
+
validatePhaseGateAtPaths(paths, amendment.phase, JSON.parse(readFileSync(amendment.ledgerPath, 'utf8')), {
|
|
1283
|
+
providers: [...state.phases, ...state.adjuncts]
|
|
1284
|
+
.find((phase) => phase.id === amendment.phase)?.validation?.providers ?? state.providers ?? [],
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
atomicJson(receiptPath, receipt);
|
|
1288
|
+
input.afterAmendmentFilesWritten?.();
|
|
1289
|
+
persistTransition(paths, state);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
for (const snapshot of ledgerSnapshots) {
|
|
1292
|
+
if (snapshot.existed) atomicText(snapshot.path, snapshot.content);
|
|
1293
|
+
else rmSync(snapshot.path, { force: true });
|
|
1294
|
+
}
|
|
1295
|
+
if (receiptSnapshot.existed) atomicText(receiptSnapshot.path, receiptSnapshot.content);
|
|
1296
|
+
else rmSync(receiptPath, { force: true });
|
|
1297
|
+
if (!rootExisted && existsSync(amendmentRoot)) rmdirSync(amendmentRoot);
|
|
1298
|
+
throw error;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return {
|
|
1302
|
+
action: 'ratified',
|
|
1303
|
+
receipt,
|
|
1304
|
+
receiptPath: relative(paths.deliveryRoot, receiptPath).replaceAll('\\', '/'),
|
|
1305
|
+
state,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
export function recordBuildApproval(projectRoot, slug, input = {}) {
|
|
1310
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
1311
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
1312
|
+
assertIntentCopiesConsistent(paths, state);
|
|
1313
|
+
assertCompletedEvidenceFresh(paths, state);
|
|
1314
|
+
if (state.status !== 'in-progress' || state.currentPhase !== 'build' || nextRequiredPhase(state) !== 'build') {
|
|
1315
|
+
throw new Error('Build approval can be recorded only after all pre-Build gates are complete in an active delivery.');
|
|
1316
|
+
}
|
|
1317
|
+
if (!input.approvedBy) throw new Error('Build approval requires the approving person or role.');
|
|
1318
|
+
if (input.decision !== 'approved') throw new Error('Build approval decision must be approved.');
|
|
1319
|
+
const approvedAt = now();
|
|
1320
|
+
const securityPolicy = captureSecurityPolicy(paths.projectRoot, { capturedAt: approvedAt });
|
|
1321
|
+
const approval = {
|
|
1322
|
+
schema: 'ewai.build-approval/v1',
|
|
1323
|
+
slug,
|
|
1324
|
+
decision: 'approved',
|
|
1325
|
+
approvedBy: input.approvedBy,
|
|
1326
|
+
approvedAt,
|
|
1327
|
+
scope: input.scope ?? '',
|
|
1328
|
+
stateSha256: sha256(readFileSync(paths.statePath)),
|
|
1329
|
+
securityPolicy,
|
|
1330
|
+
};
|
|
1331
|
+
const approvalPath = resolve(paths.gatesRoot, 'build/build-approval.json');
|
|
1332
|
+
const approvalBefore = fileSnapshot(approvalPath);
|
|
1333
|
+
atomicJson(approvalPath, approval);
|
|
1334
|
+
state.approvals.build = {
|
|
1335
|
+
path: relative(paths.projectRoot, approvalPath).replaceAll('\\', '/'),
|
|
1336
|
+
approvedBy: approval.approvedBy,
|
|
1337
|
+
approvedAt: approval.approvedAt,
|
|
1338
|
+
scope: approval.scope,
|
|
1339
|
+
securityPolicy,
|
|
1340
|
+
};
|
|
1341
|
+
try {
|
|
1342
|
+
persistTransition(paths, state);
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (approvalBefore.existed) atomicText(approvalPath, approvalBefore.content);
|
|
1345
|
+
else rmSync(approvalPath, { force: true });
|
|
1346
|
+
throw error;
|
|
1347
|
+
}
|
|
1348
|
+
publishDeliveryLifecycle(paths, state, 'ewai.delivery.build.approved', {
|
|
1349
|
+
sourceKey: `delivery:${state.slug}:build.approved:${approval.approvedAt}`,
|
|
1350
|
+
occurredAt: approval.approvedAt,
|
|
1351
|
+
sourceRevision: state.updatedAt,
|
|
1352
|
+
facts: { status: 'approved', decision: 'approved' },
|
|
1353
|
+
evidence: [relative(paths.projectRoot, approvalPath).replaceAll('\\', '/')],
|
|
1354
|
+
now: approval.approvedAt,
|
|
1355
|
+
});
|
|
1356
|
+
addActivityEvent(paths.projectRoot, state.intent.id, {
|
|
1357
|
+
eventType: 'decision',
|
|
1358
|
+
phaseKey: 'build',
|
|
1359
|
+
summary: `Build approved by ${approval.approvedBy}.`,
|
|
1360
|
+
details: approval.scope
|
|
1361
|
+
});
|
|
1362
|
+
return approval;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
export function recordManualQaApproval(projectRoot, slug, input = {}) {
|
|
1366
|
+
const paths = deliveryPaths(projectRoot, slug);
|
|
1367
|
+
const state = readDeliveryState(projectRoot, slug);
|
|
1368
|
+
assertIntentCopiesConsistent(paths, state);
|
|
1369
|
+
assertCompletedEvidenceFresh(paths, state);
|
|
1370
|
+
if (nextRequiredPhase(state) !== 'manual-qa') {
|
|
1371
|
+
throw new Error('Manual QA approval can be recorded only after Delivery is complete.');
|
|
1372
|
+
}
|
|
1373
|
+
if (!input.approvedBy) throw new Error('Manual QA approval requires the approving person or role.');
|
|
1374
|
+
if (input.decision !== 'approved') throw new Error('Manual QA decision must be approved.');
|
|
1375
|
+
if (!input.evidencePath) throw new Error('Manual QA approval requires a project-owned evidence file.');
|
|
1376
|
+
const evidence = evidenceAbsolute(paths.projectRoot, input.evidencePath);
|
|
1377
|
+
const approval = {
|
|
1378
|
+
schema: 'ewai.manual-qa-approval/v1',
|
|
1379
|
+
slug,
|
|
1380
|
+
decision: 'approved',
|
|
1381
|
+
approvedBy: input.approvedBy,
|
|
1382
|
+
approvedAt: now(),
|
|
1383
|
+
evidencePath: relative(paths.projectRoot, evidence).replaceAll('\\', '/'),
|
|
1384
|
+
evidenceSha256: sha256(readFileSync(evidence)),
|
|
1385
|
+
notes: input.notes ?? ''
|
|
1386
|
+
};
|
|
1387
|
+
const approvalPath = resolve(paths.gatesRoot, 'manual-qa/approval.json');
|
|
1388
|
+
const approvalBefore = fileSnapshot(approvalPath);
|
|
1389
|
+
atomicJson(approvalPath, approval);
|
|
1390
|
+
const gate = state.humanGates.find((candidate) => candidate.id === 'manual-qa');
|
|
1391
|
+
gate.status = 'approved';
|
|
1392
|
+
gate.completedAt = approval.approvedAt;
|
|
1393
|
+
gate.approvedBy = approval.approvedBy;
|
|
1394
|
+
gate.approvedAt = approval.approvedAt;
|
|
1395
|
+
gate.evidencePath = approval.evidencePath;
|
|
1396
|
+
state.currentPhase = nextRequiredPhase(state);
|
|
1397
|
+
state.status = 'in-progress';
|
|
1398
|
+
try {
|
|
1399
|
+
persistTransition(paths, state);
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
if (approvalBefore.existed) atomicText(approvalPath, approvalBefore.content);
|
|
1402
|
+
else rmSync(approvalPath, { force: true });
|
|
1403
|
+
throw error;
|
|
1404
|
+
}
|
|
1405
|
+
publishDeliveryLifecycle(paths, state, 'ewai.delivery.manual-qa.approved', {
|
|
1406
|
+
sourceKey: `delivery:${state.slug}:manual-qa.approved:${approval.approvedAt}`,
|
|
1407
|
+
occurredAt: approval.approvedAt,
|
|
1408
|
+
sourceRevision: state.updatedAt,
|
|
1409
|
+
facts: { status: 'approved', decision: 'approved' },
|
|
1410
|
+
evidence: [approval.evidencePath],
|
|
1411
|
+
now: approval.approvedAt,
|
|
1412
|
+
});
|
|
1413
|
+
const releaseReadiness = assessSecurityReadiness(paths.projectRoot, {
|
|
1414
|
+
policySnapshot: state.approvals?.build?.securityPolicy,
|
|
1415
|
+
});
|
|
1416
|
+
if (releaseReadiness.ready) {
|
|
1417
|
+
publishDeliveryLifecycle(paths, state, 'ewai.delivery.release-ready', {
|
|
1418
|
+
sourceKey: `delivery:${state.slug}:release-ready:${approval.approvedAt}`,
|
|
1419
|
+
occurredAt: approval.approvedAt,
|
|
1420
|
+
sourceRevision: state.updatedAt,
|
|
1421
|
+
facts: { status: 'ready' },
|
|
1422
|
+
evidence: [approval.evidencePath],
|
|
1423
|
+
now: approval.approvedAt,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
addActivityEvent(paths.projectRoot, state.intent.id, {
|
|
1427
|
+
eventType: 'decision',
|
|
1428
|
+
phaseKey: 'manual-qa',
|
|
1429
|
+
summary: `Manual QA approved by ${approval.approvedBy}.`,
|
|
1430
|
+
details: approval.evidencePath
|
|
1431
|
+
});
|
|
1432
|
+
return { ...approval, releaseReadiness };
|
|
1433
|
+
}
|