@mriekki/project-forge 4.6.0 → 4.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.
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import sys
7
+ from pathlib import Path, PurePosixPath
8
+
9
+ ROOT = Path(__file__).resolve().parents[2]
10
+
11
+
12
+ def read_json(name: str) -> dict:
13
+ path = ROOT / name
14
+ try:
15
+ value = json.loads(path.read_text(encoding="utf-8"))
16
+ except Exception as exc:
17
+ raise RuntimeError(f"Unable to read {name}: {exc}") from exc
18
+ if not isinstance(value, dict):
19
+ raise RuntimeError(f"{name} must contain a JSON object")
20
+ return value
21
+
22
+
23
+ def safe_relative(value: object, label: str) -> str:
24
+ if not isinstance(value, str) or not value:
25
+ raise RuntimeError(f"{label} must be a non-empty relative path")
26
+ normalized = value.replace("\\", "/")
27
+ pure = PurePosixPath(normalized)
28
+ if pure.is_absolute() or not pure.parts or any(part in {"", ".", ".."} for part in pure.parts):
29
+ raise RuntimeError(f"Unsafe {label}: {value}")
30
+ return normalized
31
+
32
+
33
+ def sha256(path: Path) -> str:
34
+ digest = hashlib.sha256()
35
+ with path.open("rb") as handle:
36
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
37
+ digest.update(chunk)
38
+ return digest.hexdigest()
39
+
40
+
41
+ def project_component_metadata(component: str, metadata: dict, schema_version: int) -> dict:
42
+ if schema_version == 1:
43
+ result = {
44
+ "revision": metadata.get("commit"),
45
+ "distribution": metadata.get("distribution"),
46
+ }
47
+ if component == "core":
48
+ result["version"] = metadata.get("version")
49
+ return result
50
+ if schema_version == 2:
51
+ result = {
52
+ "revision": metadata.get("revision"),
53
+ "distribution": metadata.get("distribution"),
54
+ }
55
+ if component == "core":
56
+ result["version"] = metadata.get("version")
57
+ return result
58
+ raise RuntimeError("Unsupported PROJECT_FORGE_BASELINE.json schemaVersion")
59
+
60
+
61
+ def component_baseline_metadata(component: str, baseline: dict, filename: str) -> dict:
62
+ schema_version = baseline.get("schemaVersion")
63
+ if schema_version == 1:
64
+ if component == "core":
65
+ return {
66
+ "revision": baseline.get("coreCommit"),
67
+ "version": baseline.get("coreVersion"),
68
+ "distribution": baseline.get("distribution"),
69
+ }
70
+ if component == "deployment":
71
+ return {
72
+ "revision": baseline.get("deploymentKitCommit"),
73
+ "distribution": baseline.get("distribution"),
74
+ }
75
+ elif schema_version == 2:
76
+ if baseline.get("component") != component:
77
+ raise RuntimeError(f"{filename} component does not match PROJECT_FORGE_BASELINE.json")
78
+ result = {
79
+ "revision": baseline.get("revision"),
80
+ "distribution": baseline.get("distribution"),
81
+ }
82
+ if component == "core":
83
+ result["version"] = baseline.get("version")
84
+ return result
85
+ else:
86
+ raise RuntimeError(f"Unsupported {filename} schemaVersion")
87
+ raise RuntimeError(f"Unsupported Project Forge component in generated baseline: {component}")
88
+
89
+
90
+ def validate_component(
91
+ component: str,
92
+ project_forge_component: dict,
93
+ filename: str,
94
+ project_schema_version: int,
95
+ ) -> int:
96
+ baseline = read_json(filename)
97
+ if baseline.get("schemaVersion") != project_schema_version:
98
+ raise RuntimeError(
99
+ f"{filename} schemaVersion does not match PROJECT_FORGE_BASELINE.json"
100
+ )
101
+
102
+ expected = project_component_metadata(component, project_forge_component, project_schema_version)
103
+ actual = component_baseline_metadata(component, baseline, filename)
104
+ for key, expected_value in expected.items():
105
+ if expected_value is not None and actual.get(key) != expected_value:
106
+ raise RuntimeError(
107
+ f"{filename} {key} does not match PROJECT_FORGE_BASELINE.json"
108
+ )
109
+
110
+ managed = baseline.get("managedPaths")
111
+ if not isinstance(managed, list) or not managed:
112
+ raise RuntimeError(f"{filename} managedPaths is missing or empty")
113
+
114
+ count = 0
115
+ seen: set[str] = set()
116
+ for item in managed:
117
+ if not isinstance(item, dict):
118
+ raise RuntimeError(f"Invalid managedPaths entry in {filename}")
119
+ relative = safe_relative(item.get("path"), f"{filename} managed path")
120
+ expected_hash = item.get("sha256")
121
+ if not isinstance(expected_hash, str) or len(expected_hash) != 64:
122
+ raise RuntimeError(f"Invalid managed hash in {filename}: {relative}")
123
+ if relative in seen:
124
+ raise RuntimeError(f"Duplicate managed path in {filename}: {relative}")
125
+ seen.add(relative)
126
+ path = ROOT.joinpath(*PurePosixPath(relative).parts)
127
+ if path.is_symlink() or not path.is_file():
128
+ raise RuntimeError(f"Managed file missing or unsafe: {relative}")
129
+ actual_hash = sha256(path)
130
+ if actual_hash != expected_hash:
131
+ raise RuntimeError(f"Managed file drift: {relative}")
132
+ count += 1
133
+ return count
134
+
135
+
136
+ def main() -> int:
137
+ try:
138
+ manifest = read_json("PROJECT_BOOTSTRAP.json")
139
+ approved = (manifest.get("generation") or {}).get("project-forge") or {}
140
+ project_forge = read_json("PROJECT_FORGE_BASELINE.json")
141
+ project_schema_version = project_forge.get("schemaVersion")
142
+ if project_schema_version not in {1, 2}:
143
+ raise RuntimeError("Unsupported PROJECT_FORGE_BASELINE.json schemaVersion")
144
+ for key in ["package", "version", "profile"]:
145
+ if project_forge.get(key) != approved.get(key):
146
+ raise RuntimeError(
147
+ f"PROJECT_FORGE_BASELINE.json {key} does not match PROJECT_BOOTSTRAP.json"
148
+ )
149
+
150
+ components = project_forge.get("components")
151
+ if not isinstance(components, dict) or not components:
152
+ raise RuntimeError("PROJECT_FORGE_BASELINE.json components is missing or empty")
153
+
154
+ total = 0
155
+ for component, metadata in sorted(components.items()):
156
+ if component == "core":
157
+ filename = "CORE_BASELINE.json"
158
+ elif component == "deployment":
159
+ filename = "DEPLOYMENT_BASELINE.json"
160
+ else:
161
+ raise RuntimeError(f"Unsupported Project Forge component: {component}")
162
+ if not isinstance(metadata, dict):
163
+ raise RuntimeError(f"Invalid Project Forge component metadata: {component}")
164
+ total += validate_component(
165
+ component,
166
+ metadata,
167
+ filename,
168
+ project_schema_version,
169
+ )
170
+ print(f"PASS {filename}")
171
+
172
+ print(f"PASS Project Forge managed baselines ({total} files)")
173
+ return 0
174
+ except RuntimeError as exc:
175
+ print(f"FAIL Project Forge managed baselines: {exc}", file=sys.stderr)
176
+ return 2
177
+
178
+
179
+ if __name__ == "__main__":
180
+ raise SystemExit(main())
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "bootstrap-managed-baseline-verifier-schema2",
4
+ "owner": "bootstrap",
5
+ "destination": "tools/project-forge-package/check-managed-baselines.py",
6
+ "asset": "assets/bootstrap-migrations/managed-baseline-verifier/check-managed-baselines.py",
7
+ "source": {
8
+ "component": "bootstrap",
9
+ "revision": "b5a83729d211e0492be5368ff7f1e59b63986419",
10
+ "artifact": "migration-assets/package-workspace/check-managed-baselines.py"
11
+ },
12
+ "targetGitBlob": "26496854ed5d6ad47c7b6136078b2114473ac773",
13
+ "supportedPredecessorGitBlobs": [
14
+ "f7ccfc0e8efaac4d6aed6ed5d8e2130d4fb3dc9d"
15
+ ],
16
+ "supportedProjectBaselineSchemas": [1, 2]
17
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.6.0",
4
+ "version": "4.8.0",
5
5
  "defaultProfile": "greenfield-project",
6
6
  "profiles": {
7
7
  "greenfield-project": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "components": {
15
15
  "core": {
16
- "revision": "ff91df577814d4ef398fe5a931f8a8343a5c024c",
16
+ "revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
17
17
  "version": "4.0.0",
18
18
  "distribution": "greenfield-workspace",
19
19
  "descriptor": "assets/core/distribution.json",
@@ -21,7 +21,7 @@
21
21
  "canonicalDescriptor": "distributions/greenfield-workspace.json"
22
22
  },
23
23
  "deployment": {
24
- "revision": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
24
+ "revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
25
25
  "distribution": "greenfield-project",
26
26
  "descriptor": "assets/deployment/distribution.json",
27
27
  "sourceRoot": "assets/generated/deployment",
@@ -6,6 +6,8 @@
6
6
  {"source": "distribution/project-forge-deploy.py", "destination": "tools/project-forge-deployment/project-forge-deploy.py"},
7
7
  {"source": "src/project_forge_deployment/__init__.py", "destination": "tools/project-forge-deployment/project_forge_deployment/__init__.py"},
8
8
  {"source": "src/project_forge_deployment/__main__.py", "destination": "tools/project-forge-deployment/project_forge_deployment/__main__.py"},
9
+ {"source": "src/project_forge_deployment/access_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/access_cli.py"},
10
+ {"source": "src/project_forge_deployment/access_diagnosis.py", "destination": "tools/project-forge-deployment/project_forge_deployment/access_diagnosis.py"},
9
11
  {"source": "src/project_forge_deployment/app_cli.py", "destination": "tools/project-forge-deployment/project_forge_deployment/app_cli.py"},
10
12
  {"source": "src/project_forge_deployment/app_deployment.py", "destination": "tools/project-forge-deployment/project_forge_deployment/app_deployment.py"},
11
13
  {"source": "src/project_forge_deployment/app_runtime.py", "destination": "tools/project-forge-deployment/project_forge_deployment/app_runtime.py"},
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.6.0",
4
+ "version": "4.8.0",
5
5
  "components": {
6
6
  "core": {
7
- "revision": "ff91df577814d4ef398fe5a931f8a8343a5c024c",
7
+ "revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
8
8
  "version": "4.0.0",
9
9
  "distribution": "greenfield-workspace",
10
10
  "files": [
@@ -51,7 +51,7 @@
51
51
  {
52
52
  "source": "core/skills/setup/SKILL.md",
53
53
  "destination": ".github/skills/setup/SKILL.md",
54
- "sha256": "0ba13a009347742ede2c6e6121a21f29640a05864a994c7a51b4c992c0b2c619"
54
+ "sha256": "27419e0d330abf2b6b03a696eba352ab2ece13a616c41fc0ade9aed85e8834f1"
55
55
  },
56
56
  {
57
57
  "source": "core/skills/build/SKILL.md",
@@ -116,12 +116,12 @@
116
116
  {
117
117
  "source": "core/skills/deployment-readiness/SKILL.md",
118
118
  "destination": ".github/skills/deployment-readiness/SKILL.md",
119
- "sha256": "33be75e856b1b16378bb2a456d8418f8ae0b03edee75fb826d8f7ef3824d2f6a"
119
+ "sha256": "30202702cc368c872e62ce731e8ffe84a796aae243b287e3e670c8c8e1cd6531"
120
120
  },
121
121
  {
122
122
  "source": "core/skills/deploy/SKILL.md",
123
123
  "destination": ".github/skills/deploy/SKILL.md",
124
- "sha256": "21b8209bc756ef2981f9e44f19ee80e79c1c5c7d5be4c03c9b528fdadd7b8eca"
124
+ "sha256": "f5d40e549ca958cb51606ced7ca482651d9a436327b6070a9a94171f22401700"
125
125
  },
126
126
  {
127
127
  "source": "core/skills/tear-down/SKILL.md",
@@ -326,7 +326,7 @@
326
326
  ]
327
327
  },
328
328
  "deployment": {
329
- "revision": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
329
+ "revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
330
330
  "distribution": "greenfield-project",
331
331
  "files": [
332
332
  {
@@ -344,15 +344,25 @@
344
344
  "destination": "tools/project-forge-deployment/project_forge_deployment/__main__.py",
345
345
  "sha256": "8742c1b766812edb07174a724d676ffeff58b71b3c192d35102a3e652e962946"
346
346
  },
347
+ {
348
+ "source": "src/project_forge_deployment/access_cli.py",
349
+ "destination": "tools/project-forge-deployment/project_forge_deployment/access_cli.py",
350
+ "sha256": "007d01ae0c4e28e64f75fb96f9e8e7b99db5c4c8129698a78923baeac2f51953"
351
+ },
352
+ {
353
+ "source": "src/project_forge_deployment/access_diagnosis.py",
354
+ "destination": "tools/project-forge-deployment/project_forge_deployment/access_diagnosis.py",
355
+ "sha256": "92861e58a29b17758a927f7a527b089f172a1bc0c9725c79731ef07e00294af1"
356
+ },
347
357
  {
348
358
  "source": "src/project_forge_deployment/app_cli.py",
349
359
  "destination": "tools/project-forge-deployment/project_forge_deployment/app_cli.py",
350
- "sha256": "9bf8eb7337c5b506311bf718de71b9a2c9ac445eb94cb7dfdeb63a09e5f0f899"
360
+ "sha256": "f27704c80b1eff45888bf56745bcf8755f2fad88851147600af7056953757825"
351
361
  },
352
362
  {
353
363
  "source": "src/project_forge_deployment/app_deployment.py",
354
364
  "destination": "tools/project-forge-deployment/project_forge_deployment/app_deployment.py",
355
- "sha256": "e58158e48c9fbd51b51f7af90f2dd178987ad3936e44507523cfc41b96fcd9f9"
365
+ "sha256": "606d6a4793ac8c8a95da4cd4c61792c69f02ad86900be1944d2ed570dd71e73e"
356
366
  },
357
367
  {
358
368
  "source": "src/project_forge_deployment/app_runtime.py",
@@ -397,7 +407,7 @@
397
407
  {
398
408
  "source": "src/project_forge_deployment/entrypoint.py",
399
409
  "destination": "tools/project-forge-deployment/project_forge_deployment/entrypoint.py",
400
- "sha256": "341e4682a050e9eea8c6ecf0e8728efacc57f06144d9eceb12ac37cf8fb2a3b4"
410
+ "sha256": "6b767380f502acf0763b71f46aff6efd62674e63a526ed270097efa68c320a1e"
401
411
  },
402
412
  {
403
413
  "source": "src/project_forge_deployment/environment_cli.py",
@@ -492,12 +502,12 @@
492
502
  {
493
503
  "source": "src/project_forge_deployment/registry_access.py",
494
504
  "destination": "tools/project-forge-deployment/project_forge_deployment/registry_access.py",
495
- "sha256": "5815a1921ecf37dc395f9c68b1ebda2b17dd6a44620b2356d1adff5df44c4382"
505
+ "sha256": "1f582f1b5da656579625748b13c779f6d67ed6a6049f0ebb104ce8c3f9185a4b"
496
506
  },
497
507
  {
498
508
  "source": "src/project_forge_deployment/registry_cli.py",
499
509
  "destination": "tools/project-forge-deployment/project_forge_deployment/registry_cli.py",
500
- "sha256": "1936156eb3b6103895795cde3b5f520491e407bf35f311be883a54fbfff635fc"
510
+ "sha256": "2ce977fa570580407c04a3e59d0a524f128af420b7ee20fd9706e6452378b7b3"
501
511
  },
502
512
  {
503
513
  "source": "src/project_forge_deployment/route_registry.py",
@@ -512,7 +522,7 @@
512
522
  {
513
523
  "source": "src/project_forge_deployment/security.py",
514
524
  "destination": "tools/project-forge-deployment/project_forge_deployment/security.py",
515
- "sha256": "f0946186e384794c81a78fbf50b6afdbbd79a0b22216034778fbb24d523696b6"
525
+ "sha256": "d24d4faa0d98b2e7484e2a9e959ba4624927be61a84453ed2db150aa6132c0cf"
516
526
  },
517
527
  {
518
528
  "source": "src/project_forge_deployment/sqlite_adapter.py",
@@ -44,7 +44,7 @@ Read only what is needed, including when present:
44
44
  - `PROJECT_FORGE_BASELINE.json`;
45
45
  - `DEPLOYMENT_BASELINE.json`;
46
46
  - project topology and approved deployment/release requirements;
47
- - `/deployment-readiness` evidence, including deterministic application isolation evidence when supported;
47
+ - `/deployment-readiness` evidence, including first-application outcome assessment and deterministic application isolation evidence when supported;
48
48
  - fresh setup-time environment-proof evidence for this workstation;
49
49
  - established application deployment facts.
50
50
 
@@ -62,7 +62,27 @@ Proof does not establish the real application's image/release, persistence/migra
62
62
 
63
63
  A prior readiness isolation fingerprint is review evidence only. It does not authorize deployment and does not replace the deterministic deployment planner's current isolation revalidation.
64
64
 
65
- ## 3. First real deployment asks only unresolved application questions
65
+ For a first real deployment, do not collapse a target/platform `READY` result into application deployability. Confirm that readiness established an actual artifact build/publication/acquisition path when that path is material. If artifact identity or acquisition is still hypothetical, return to the missing prerequisite rather than requesting the application write gate.
66
+
67
+ ## 3. Preserve execution and credential context
68
+
69
+ When installed tooling exposes structured capability evidence, preserve its semantics exactly in the deployment assessment and preview. In particular, retain fields equivalent to:
70
+
71
+ ```text
72
+ executionLocation
73
+ credentialContext
74
+ capabilityProved
75
+ capabilityAttempted
76
+ doesNotProve
77
+ ```
78
+
79
+ A registry manifest check executed on a remote target proves only the capability reported for that target/context. Never describe it as workstation registry access. A workstation registry check proves only the workstation capability it reports. Neither implies Actions registry transport or package-metadata API access.
80
+
81
+ Do not infer provider-specific token scopes or error meaning from generic registry results. Provider-specific package metadata/publication behavior belongs to repository/provider tooling and its own evidence.
82
+
83
+ When a new credential is genuinely required, explain its purpose, execution context, storage location, retrievability, and rotation impact before asking the developer to create it. Credential values remain in the provider/local secret surface and never enter chat.
84
+
85
+ ## 4. First real deployment asks only unresolved application questions
66
86
 
67
87
  The first real `/deploy <environment>` after environment proof may need a small application-specific completion step. Ask only material unknowns not already answered by project evidence, such as:
68
88
 
@@ -79,7 +99,7 @@ When a bounded choice is useful, follow `INTERACTIVE_SESSION_GUIDE.md`: use host
79
99
 
80
100
  Once these facts are durably established, later deployments should be routine rather than repeating onboarding.
81
101
 
82
- ## 4. Placement determines launcher
102
+ ## 5. Placement determines launcher
83
103
 
84
104
  ### Workstation container
85
105
 
@@ -114,7 +134,7 @@ Use only an installed repository/catalog provider adapter. If none exists, stop
114
134
 
115
135
  Report `DEPLOYMENT NOT BOUND YET` and route to `/setup <environment>` or `/deployment-readiness <environment>`.
116
136
 
117
- ## 5. Preserve custom Production classification
137
+ ## 6. Preserve custom Production classification
118
138
 
119
139
  Pass the exact configured environment name to the deterministic launcher.
120
140
 
@@ -126,7 +146,7 @@ When resolved purpose/classification is Production, use the installed tool's exp
126
146
 
127
147
  Inspect installed help/version evidence. If the installed tool cannot preserve Production classification for a custom name, stop rather than downgrading safety.
128
148
 
129
- ## 6. Build the deterministic plan
149
+ ## 7. Build the deterministic plan
130
150
 
131
151
  Use repository-provided tooling to construct/inspect the exact application deployment plan. Supported operations may include:
132
152
 
@@ -149,7 +169,44 @@ The deterministic deployment tool owns the final current-state recheck immediate
149
169
 
150
170
  Do not smuggle infrastructure provisioning, target/provider rebinding, destructive restore, or environment tear-down into the application plan.
151
171
 
152
- ## 7. Preserve exact authorization contracts
172
+ ### Remote runtime + route rollout preview
173
+
174
+ When the installed Deployment Kit exposes a deterministic read-only `app rollout` capability and the selected remote deployment has an intended route, prefer that capability for the final pre-write preview rather than independently narrating runtime and route consequences.
175
+
176
+ A current installed contract may expose a command shape similar to:
177
+
178
+ ```text
179
+ project-forge-deploy app rollout <target>
180
+ --owner-repo <owner/repository>
181
+ --environment <exact environment>
182
+ --app-slug <application slug>
183
+ --container-name <container>
184
+ --runtime-port <port>
185
+ --persistence-mode <none|directory>
186
+ --image <immutable digest>
187
+ --health-path <path>
188
+ --run-as <uid[:gid]>
189
+ [--route-hostname <hostname> ...]
190
+ [--production]
191
+ --json
192
+ ```
193
+
194
+ Inspect installed help/version rather than assuming this example exactly matches every package version.
195
+
196
+ Treat the returned staged rollout as **read-only review evidence**. Preserve:
197
+
198
+ ```text
199
+ runtime plan + isolation evidence
200
+ route plan + route-inventory evidence
201
+ separate stage authorizations
202
+ endpoint state before runtime
203
+ after-runtime / before-route endpoint state
204
+ after-route verification state
205
+ ```
206
+
207
+ If the installed package lacks this composite preview, retain the existing safe runtime and route planners and state their sequence explicitly. Do not invent a combined write command.
208
+
209
+ ## 8. Preserve exact authorization contracts
153
210
 
154
211
  Core consequence labels do not replace stricter installed-tool phrases.
155
212
 
@@ -158,17 +215,20 @@ The standard Deployment Kit may use exact phrases such as:
158
215
  ```text
159
216
  [APP DEPLOY WRITE]
160
217
  [PRODUCTION APP DEPLOY]
218
+ [ROUTE WRITE]
161
219
  ```
162
220
 
163
221
  For Production-class environments also show `[PRODUCTION WRITE]` consequence visibility where Core policy calls for it.
164
222
 
165
- Resolve the exact phrase from the installed tool/version. Do not assume an example phrase when the installed contract differs.
223
+ Resolve the exact phrase from the installed tool/version. For a staged rollout, resolve it separately for each stage. Do not assume an example phrase when the installed contract differs.
224
+
225
+ Authorization applies only to the exact environment, resolved classification, placement/target/provider, artifact, reviewed plan/stage, current isolation evidence and route evidence, and evidence shown. Material changes require renewed review/authorization.
166
226
 
167
- Authorization applies only to the exact environment, resolved classification, placement/target/provider, artifact, reviewed plan, current isolation evidence, and evidence shown. Material changes require renewed review/authorization.
227
+ A runtime authorization never authorizes the route stage. A route authorization never retroactively authorizes a changed runtime artifact. If the rollout contains both stages, preserve the separate gates even though the Builder reviewed one coherent outcome.
168
228
 
169
- ## 8. Preview and execute only the approved contract
229
+ ## 9. Preview and execute only the approved contract
170
230
 
171
- Before mutation, show concise non-secret evidence such as exact environment, purpose, placement, target/provider when applicable, proof freshness, immutable application artifact, persistence/migration, exposure/route, application isolation state/fingerprint when exposed, shared failure-domain note when relevant, planned operations, and exact required authorization phrase.
231
+ Before mutation, show concise non-secret evidence such as exact environment, purpose, placement, target/provider when applicable, proof freshness, immutable application artifact, persistence/migration, exposure/route, application isolation state/fingerprint when exposed, shared failure-domain note when relevant, planned stages, endpoint state at each stage, and the exact required authorization phrase for the next consequence only.
172
232
 
173
233
  After authorization, invoke only the repository-provided deterministic launcher. Never redirect to another environment/host/provider, weaken trust/exposure, force through lock contention, or invent recovery writes.
174
234
 
@@ -176,7 +236,37 @@ The deterministic launcher must revalidate current isolation immediately before
176
236
 
177
237
  Existing per-app/per-environment locks are normal concurrency protection when another workstation is deploying the same environment.
178
238
 
179
- ## 9. Validate live evidence
239
+ ### Runtime succeeded but route is pending
240
+
241
+ A healthy application runtime is a valid intermediate outcome. When a requested route has not yet been registered, say this explicitly before showing the route gate:
242
+
243
+ ```text
244
+ APPLICATION RUNTIME READY
245
+ Expected endpoint: <hostname/URL>
246
+ Endpoint state: NOT ACTIVE — ROUTE PENDING
247
+ Do not test this URL yet; the route write has not been authorized/applied.
248
+ ```
249
+
250
+ Then, if the reviewed route plan is still current, present the route consequence directly. Do not require the developer to invoke `/deploy` or another slash command merely to rediscover the already reviewed route stage.
251
+
252
+ The runtime success does not consume or imply route authorization. If route evidence changed, rebuild/review the route plan first.
253
+
254
+ ## 10. Resume the established outcome without navigation ceremony
255
+
256
+ Within the same conversation, if a bounded engineering/delivery correction or external prerequisite completes and the developer's requested deployment outcome remains unambiguous, automatically resume safe **read-only** reconstruction of readiness/deployment evidence from the repository, GitHub, installed tools, and runtime state.
257
+
258
+ Do not require the developer to type `/deliver`, `/deployment-readiness`, or `/deploy` merely as navigation to rediscover known state after that bounded action completes.
259
+
260
+ This continuation:
261
+
262
+ - never repeats a completed write;
263
+ - never reuses exhausted registry/Production/route/merge/migration/destructive authority;
264
+ - stops when the artifact, plan, target, consequence, or other material evidence changed;
265
+ - does not create a parallel persistent workflow database.
266
+
267
+ If `/deploy` is invoked again after an already validated deployment, first reconstruct current deployment evidence. If the same validated artifact/runtime/route remains current and no new deployment outcome is requested, report the existing validated state rather than proposing or performing a redundant redeployment.
268
+
269
+ ## 11. Validate live evidence
180
270
 
181
271
  After execution, use repository-provided inventory/health/route/smoke/evidence commands when available.
182
272
 
@@ -191,10 +281,32 @@ Artifact: <exact immutable identity>
191
281
  Artifact delivery: <workstation-transfer|remote-registry|local-runtime|provider>
192
282
  Endpoint/Route: READY / LOOPBACK / NOT APPLICABLE
193
283
  Application health: READY
284
+ Workstation/browser verification: PASSED / NOT APPLICABLE / PENDING
194
285
  Smoke: PASSED / NOT REQUIRED
195
286
  Deployment evidence: RECORDED / VERIFIED
196
287
  ```
197
288
 
198
- Do not report success merely because a container/process started when stronger health evidence is required.
289
+ Do not report success merely because a container/process started when stronger health evidence is required. Do not report a route as user-accessible merely because registration returned; perform the repository-supported endpoint/workstation/browser verification when the requested outcome depends on it.
199
290
 
200
291
  A setup-time disposable environment proof is not real application deployment evidence. A successful application deployment never authorizes a later Production write or destructive `/tear-down`.
292
+
293
+ Keep human access confirmation distinct from automated browser/health evidence. The developer saying they can reach the endpoint is useful observed human evidence; it is not automatically formal work-item acceptance.
294
+
295
+ ## 12. Reconcile the durable outcome without asserting acceptance
296
+
297
+ After `DEPLOYMENT VALIDATED`, inspect the repository's configured work-item/operating authority. When an existing current-status or deployment-result section is stale and the observed deployment outcome is material, offer a bounded reconciliation containing only supported non-secret facts such as:
298
+
299
+ ```text
300
+ exact environment
301
+ release/source/artifact identity
302
+ artifact delivery method
303
+ runtime health result
304
+ route/endpoint
305
+ workstation/browser validation
306
+ explicit human access confirmation, when actually stated
307
+ known limitations and evidence boundaries
308
+ ```
309
+
310
+ Preserve historical approval records. Do not silently rewrite product requirements, broaden the milestone, mark formal acceptance, close the work item, or infer acceptance from browser accessibility.
311
+
312
+ If the human authorizes the bounded documentation update, use the repository's normal work-item/delivery workflow for that edit. Documentation reconciliation is not a reason to redeploy and does not create new Production write authority.