@mriekki/project-forge 3.0.0 → 4.1.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.
Files changed (23) hide show
  1. package/assets/components.json +4 -4
  2. package/assets/core/distribution.json +0 -4
  3. package/assets/deployment/distribution.json +1 -0
  4. package/assets/generated/ASSEMBLY.json +22 -37
  5. package/assets/generated/core/core/instructions/project-forge-workflow.instructions.md +8 -2
  6. package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +1 -2
  7. package/assets/generated/core/core/process/OPERATION_JOURNAL.md +1 -2
  8. package/assets/generated/core/core/skills/grill-me/SKILL.md +84 -46
  9. package/assets/generated/core/core/skills/project-forge-update/SKILL.md +3 -3
  10. package/assets/generated/core/core/skills/setup/SKILL.md +149 -199
  11. package/assets/generated/core/core/skills/start-work/SKILL.md +5 -6
  12. package/assets/generated/deployment/docs/DEVELOPER_ENVIRONMENTS.md +52 -12
  13. package/assets/generated/deployment/docs/ENVIRONMENT_PROOF.md +56 -6
  14. package/assets/generated/deployment/src/project_forge_deployment/app_runtime_workstation.py +103 -42
  15. package/assets/generated/deployment/src/project_forge_deployment/environment_cli.py +41 -4
  16. package/assets/generated/deployment/src/project_forge_deployment/environment_probe.py +152 -79
  17. package/assets/generated/deployment/src/project_forge_deployment/environment_setup.py +241 -0
  18. package/assets/generated/deployment/src/project_forge_deployment/image_transfer.py +51 -11
  19. package/package.json +1 -1
  20. package/assets/generated/core/core/agents/coach.agent.md +0 -69
  21. package/assets/generated/core/core/skills/core-sync/SKILL.md +0 -184
  22. package/assets/generated/core/core/skills/developer-env-mode/SKILL.md +0 -231
  23. package/assets/generated/core/core/skills/grill-with-docs/SKILL.md +0 -88
@@ -2,7 +2,7 @@
2
2
 
3
3
  Status: **CORE WORKSTATION/TARGET MECHANICS IMPLEMENTED; AUTOMATED HOST PACKAGE PREPARATION REMAINS INCREMENTAL**
4
4
 
5
- This document is the canonical developer-environment model for Project Forges. It describes the developer-facing progression from a generated repository to local development, optional Staging/Production capability, additional workstations, and additional remote boxes.
5
+ This document is the canonical developer-environment model for Project Forge. It describes the developer-facing progression from a generated repository to local development, optional Staging/Production capability, additional workstations, and additional remote boxes.
6
6
 
7
7
  The happy path intentionally hides Bootstrap/package/control-plane plumbing. Developers should see meaningful milestones and actionable setup, while the underlying repositories retain exact provenance and safety evidence.
8
8
 
@@ -44,6 +44,37 @@ The normal conversational entry point is:
44
44
 
45
45
  The first local success target is `LOCAL WORKSPACE READY`. Remote access must not block it unless the Project explicitly requires a remote dependency for local development.
46
46
 
47
+ For remote onboarding, conversational `/setup remote` or `/setup <environment>` should consume one deterministic read-only setup state machine rather than manually reconstructing low-level steps:
48
+
49
+ ```text
50
+ project-forge-deploy environment setup status <target> --json
51
+ project-forge-deploy environment setup plan <target> --json
52
+ ```
53
+
54
+ When target access and the host platform are ready, proof context can be included without performing a write:
55
+
56
+ ```text
57
+ project-forge-deploy environment setup plan <target> \
58
+ --owner-repo <owner/repo> \
59
+ --environment <logical-environment> \
60
+ --json
61
+ ```
62
+
63
+ Add `--production` only when that logical environment is Production-class. Setup status/plan never authorizes a write; they report the existing authorization phrase and exact next deterministic operation when a write is required.
64
+
65
+ Readiness is deliberately separated into:
66
+
67
+ ```text
68
+ WORKSPACE READY
69
+ TARGET DEFINITION READY
70
+ TARGET ACCESS READY
71
+ HOST PLATFORM READY
72
+ DEPLOYMENT PATH PROVEN
73
+ APPLICATION DEPLOYED
74
+ ```
75
+
76
+ The last state is not part of setup. A successful disposable proof does not deploy the Project application.
77
+
47
78
  ## Progress and next action
48
79
 
49
80
  Use `/progress` to answer what is complete, what is blocked/deferred, who owns the next action, and local/Staging/Production capability state.
@@ -101,6 +132,8 @@ An empty profile with `targets: []` is valid. A local-only developer never needs
101
132
 
102
133
  `profile sync` fetches and validates the GitHub profile, updates the validated local cache, and with `--apply` creates only missing non-conflicting local target definitions. Existing differing local definitions are never overwritten.
103
134
 
135
+ Portable identity deliberately excludes workstation-local `addressOverride`, identity-file, and trust-file state. A separately verified temporary IP therefore does not make the same portable host definition a different target.
136
+
104
137
  `profile publish` merges new non-conflicting local target definitions into the private profile and fails on conflicts rather than silently replacing remote definitions.
105
138
 
106
139
  ### Workstation-local state
@@ -116,6 +149,8 @@ Each workstation owns its own:
116
149
 
117
150
  Private SSH keys are never synchronized between computers. A Mark Windows key, Mark Mac key, Kyle Mac key, and Daniel Windows key are distinct identities even when they can reach the same remote server.
118
151
 
152
+ The deterministic doctor resolves and reports the actual SSH executable selected from the workstation environment; all Deployment Kit SSH operations use that resolved toolchain rather than asking the developer to mix shell-specific SSH clients manually.
153
+
119
154
  ## Home Lab remote onboarding
120
155
 
121
156
  The implemented Home Lab path is password-assisted bootstrap followed by normal strict key-only operation.
@@ -164,7 +199,7 @@ The first Project on a computer may install/verify required local tools and Dock
164
199
 
165
200
  ### Remote Ubuntu target
166
201
 
167
- Target access can now be established from the local workstation, but full automatic Ubuntu package preparation is still an incremental capability. The desired host progression remains:
202
+ Target access can be established from the local workstation, but full automatic Ubuntu package preparation is still an incremental capability. The desired host progression remains:
168
203
 
169
204
  ```text
170
205
  Ubuntu/sudo verification
@@ -175,7 +210,7 @@ Ubuntu/sudo verification
175
210
  → TARGET READY
176
211
  ```
177
212
 
178
- The current platform doctor/install contract still requires host prerequisites to exist before shared-platform installation. Until a reviewed host-package bootstrap is added, `/setup remote` must report missing host packages as an actionable preparation gap rather than pretending they were installed.
213
+ The current platform doctor/install contract still requires host prerequisites to exist before shared-platform installation. Until a reviewed host-package bootstrap is added, `/setup remote` reports missing host packages as an actionable preparation gap rather than pretending they were installed. The deterministic setup plan keeps this as a distinct `HOST PLATFORM READY` blocker.
179
214
 
180
215
  Healthy existing Docker/Project Forge installations are adopted/verified, not recreated. Ambiguous/incompatible existing state fails closed rather than being overwritten.
181
216
 
@@ -188,13 +223,16 @@ The implemented workstation-mediated artifact path is:
188
223
  ```text
189
224
  local GitHub/GHCR authentication when needed
190
225
  → acquire/build exact immutable image locally
191
- → verify exact identity/digest
226
+ → preserve registry RepoDigest as declared/release identity
227
+ → resolve local Docker config/image ID
192
228
  → stream Docker image over verified SSH
193
229
  → remote docker load
194
- → verify exact image remotely
195
- → deploy/health-check using the preloaded image
230
+ → verify the same config/image ID remotely
231
+ → deploy/health-check by that verified loaded image ID
196
232
  ```
197
233
 
234
+ Docker `save`/`load` does not guarantee that a registry RepoDigest remains inspectable as a remote RepoDigest after load. Project Forge therefore uses two explicit identities: registry digest for reviewed release provenance, and Docker config/image ID for transport verification and the preloaded runtime reference.
235
+
198
236
  Remote Home Lab boxes therefore do not need a developer's persistent GitHub PAT for the normal workstation-mediated deployment path.
199
237
 
200
238
  Application deployment selects this with:
@@ -203,7 +241,7 @@ Application deployment selects this with:
203
241
  --image-source workstation
204
242
  ```
205
243
 
206
- The legacy `--image-source remote-registry` path remains available during migration/for explicit target-side registry use.
244
+ The legacy `--image-source remote-registry` path remains available for explicit target-side registry use.
207
245
 
208
246
  A credential needed to acquire a private image is entered only through the local Docker/GitHub credential boundary. Never paste it into chat or put it on a command line.
209
247
 
@@ -275,7 +313,7 @@ When GitHub cannot be reached, profile sync can use the validated last-known-goo
275
313
 
276
314
  ## Migration for existing developers
277
315
 
278
- Existing Mark/Kyle environments should migrate fully to this model rather than remain permanently dual-mode.
316
+ Existing developer environments should migrate fully to this model rather than remain permanently dual-mode.
279
317
 
280
318
  The migration contract is:
281
319
 
@@ -284,8 +322,8 @@ inventory existing workstation/targets
284
322
  → ensure private personal project-forge-profile + validated cache
285
323
  → adopt healthy existing Docker/SSH/trust where valid
286
324
  → establish workstation-mediated artifact delivery
287
- → prove Staging end-to-end
288
- → verify Production access/readiness without deploying
325
+ → prove a non-Production environment end-to-end
326
+ → verify Production access/readiness without deploying when Production is configured
289
327
  → remove/archive superseded legacy configuration only after verification
290
328
  ```
291
329
 
@@ -325,11 +363,13 @@ The intended small human surface is:
325
363
 
326
364
  ```text
327
365
  /setup
366
+ /setup local
367
+ /setup remote
368
+ /setup <environment>
328
369
  /progress
329
370
  /next-step
330
- /developer-env-mode status|local|remote
331
371
  /deployment-readiness <environment>
332
372
  /deploy <environment>
333
373
  ```
334
374
 
335
- The first three are the normal guided experience. `project-forge-deploy environment ...` commands are deterministic implementation/fallback surfaces and can be invoked directly when desired.
375
+ `/setup` is the single setup front door; the local/remote/environment forms select its scope rather than introducing a separate developer-environment command. `project-forge-deploy environment ...` commands are deterministic implementation/fallback surfaces and can be invoked directly when desired.
@@ -13,15 +13,24 @@ this workstation
13
13
  → verified target/profile access
14
14
  → ready shared host platform
15
15
  → pull small stable probe image locally
16
- → resolve exact immutable RepoDigest
17
- → transfer exact image over verified SSH
18
- → deploy with the managed application runtime
16
+ → resolve exact immutable registry RepoDigest
17
+ → resolve local Docker config/image ID
18
+ → transfer image over verified SSH
19
+ → verify the same config/image ID after remote docker load
20
+ → deploy with the managed application runtime using that verified loaded image ID
19
21
  → verify HTTP health
20
22
  → tear down the reserved probe runtime
21
- → verify cleanup evidence
23
+ → verify cleanup and image-cache evidence
22
24
  ```
23
25
 
24
- The current default probe image tag is `nginx:alpine`. The tag is never treated as the deployed identity: Project Forge pulls it locally and resolves an exact `@sha256:` RepoDigest before image transfer or remote runtime mutation.
26
+ The current default probe image tag is `nginx:alpine`. The mutable tag is never treated as deployed identity.
27
+
28
+ Project Forge deliberately records two exact identities because Docker `save`/`load` does not guarantee that a registry RepoDigest remains inspectable under the same repository reference after load:
29
+
30
+ - **registry digest** — immutable reviewed artifact/release identity (`repository@sha256:...`);
31
+ - **Docker config/image ID** — content identity used to verify the workstation-to-target transfer and to run the preloaded image (`sha256:...`).
32
+
33
+ The registry digest remains in the deployment spec and release/proof evidence. The loaded runtime is verified and launched by the matching Docker config/image ID. A target that already has that exact image ID can reuse it without retransferring the image.
25
34
 
26
35
  The probe uses:
27
36
 
@@ -33,6 +42,20 @@ The probe uses:
33
42
  - the existing per-app/per-environment deployment lock;
34
43
  - automatic cleanup through the existing scoped tear-down machinery.
35
44
 
45
+ ## Setup orchestration
46
+
47
+ Before running proof commands directly, the read-only remote setup orchestrator can show whether target access and the host platform are actually ready:
48
+
49
+ ```text
50
+ project-forge-deploy environment setup status <target> --json
51
+ project-forge-deploy environment setup plan <target> \
52
+ --owner-repo <owner/repo> \
53
+ --environment <name> \
54
+ --json
55
+ ```
56
+
57
+ These commands never perform the proof or authorize a write. They make `TARGET ACCESS READY`, `HOST PLATFORM READY`, and `DEPLOYMENT PATH PROVEN` separate states.
58
+
36
59
  ## Commands
37
60
 
38
61
  Read-only plan:
@@ -78,17 +101,44 @@ Production-class proof:
78
101
 
79
102
  A successful proof never authorizes the later real application deployment. Production proof never authorizes a Production application deployment.
80
103
 
104
+ ## Terminal evidence
105
+
106
+ A proof run returns a terminal result even when the operational path fails after the run begins. The result distinguishes planning from observed execution and includes, when available:
107
+
108
+ ```text
109
+ preflight
110
+ registryDigest
111
+ localImageId
112
+ transferState
113
+ remoteImageId
114
+ runtimeState
115
+ httpHealth
116
+ cleanupState
117
+ imageCacheState
118
+ proof
119
+ mutated
120
+ finalMutationState
121
+ error
122
+ cleanupError
123
+ ```
124
+
125
+ A read-only plan does not claim `mutated: false` as though it were the eventual run result. The run reports whether transfer/runtime mutation actually occurred and whether cleanup completed.
126
+
127
+ A failed proof exits nonzero even when structured terminal evidence was produced successfully.
128
+
81
129
  ## Cleanup
82
130
 
83
131
  The proof attempts cleanup even when deployment raises after mutation begins.
84
132
 
85
133
  Successful completion requires terminal cleanup evidence (`removed` or already `absent`) for the reserved probe runtime. If deployment succeeds but cleanup cannot be verified, the command fails rather than reporting a clean proof.
86
134
 
135
+ A loaded Docker image may remain cached after runtime cleanup. That state is reported explicitly (for example `runtime-clean-image-cached`) rather than being presented as an unmodified host. Cached image content is not application runtime or persistent application data.
136
+
87
137
  The probe must not delete another Project, another application, another logical environment, or shared host platform state.
88
138
 
89
139
  ## Relationship to `/setup` and `/deploy`
90
140
 
91
- `/setup` may run this proof after the developer selects a named remote environment to prepare on the current workstation.
141
+ `/setup remote` or `/setup <environment>` may use deterministic setup status/plan first, then run this proof after the developer selects the named remote environment and the relevant write authorization is granted.
92
142
 
93
143
  A later first real `/deploy <environment>` can reuse fresh proof evidence for target access/platform/transport mechanics and ask only unresolved application-specific questions such as persistence, health, route/exposure, and immutable application artifact policy.
94
144
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import subprocess
6
7
  from typing import Any
7
8
 
@@ -19,48 +20,42 @@ from .profiles import load_profile
19
20
  from .ssh import ssh_args, verify_access
20
21
 
21
22
 
22
- def _preloaded_script(spec: dict[str, Any]) -> str:
23
+ _IMAGE_ID_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
24
+
25
+
26
+ def _runtime_image_id(value: str) -> str:
27
+ if not isinstance(value, str) or _IMAGE_ID_RE.fullmatch(value) is None:
28
+ raise DeploymentKitError("Preloaded runtime image ID must be sha256:<64 lowercase hex>.")
29
+ return value
30
+
31
+
32
+ def _preloaded_script(spec: dict[str, Any], runtime_image_id: str) -> str:
33
+ runtime_image = _runtime_image_id(runtime_image_id)
23
34
  script = build_remote_script(spec)
24
35
  registry_block = " decode_desired\n configure_registry_auth\n"
25
36
  if registry_block not in script:
26
37
  raise DeploymentKitError("Application runtime template no longer contains the expected registry-auth hook.")
27
38
  script = script.replace(registry_block, " decode_desired\n", 1)
39
+
28
40
  pull = ' docker pull "$D_IMAGE" >/dev/null\n'
29
- replacement = ''' if ! docker image inspect "$D_IMAGE" >/dev/null 2>&1; then
30
- echo "Exact application image was not preloaded by the workstation-mediated deployment path." >&2
31
- exit 40
32
- fi
33
- '''
41
+ replacement = (
42
+ f' if ! docker image inspect "{runtime_image}" >/dev/null 2>&1; then\n'
43
+ ' echo "Exact application image ID was not preloaded by the workstation-mediated deployment path." >&2\n'
44
+ " exit 40\n"
45
+ " fi\n"
46
+ )
34
47
  if pull not in script:
35
48
  raise DeploymentKitError("Application runtime template no longer contains the expected image-pull hook.")
36
- return script.replace(pull, replacement, 1)
37
-
49
+ script = script.replace(pull, replacement, 1)
38
50
 
39
- def _run_preloaded_deploy(
40
- profile: dict[str, Any],
41
- address: str,
42
- spec: dict[str, Any],
43
- expected_spec_sha: str,
44
- ) -> None:
45
- command = ssh_args(
46
- profile,
47
- address,
48
- command="sudo -n bash -s -- deploy " + (expected_spec_sha or "NONE") + " " + profile["user"],
49
- )
50
- completed = subprocess.run(
51
- command,
52
- input=_preloaded_script(spec).encode("utf-8"),
53
- check=False,
54
- capture_output=True,
55
- )
56
- if completed.returncode != 0:
57
- stdout = completed.stdout.decode("utf-8", errors="replace")
58
- stderr = completed.stderr.decode("utf-8", errors="replace")
59
- detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
60
- raise DeploymentKitError(f"Workstation-mediated remote application deployment failed: {detail}")
51
+ runtime_hook = ' docker_args+=("$D_IMAGE")\n'
52
+ if runtime_hook not in script:
53
+ raise DeploymentKitError("Application runtime template no longer contains the expected runtime-image hook.")
54
+ script = script.replace(runtime_hook, f' docker_args+=("{runtime_image}")\n', 1)
55
+ return script
61
56
 
62
57
 
63
- def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
58
+ def _validate_plan(plan: dict[str, Any]) -> None:
64
59
  required = {
65
60
  "targetName",
66
61
  "action",
@@ -87,19 +82,67 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
87
82
  if not isinstance(plan, dict) or set(plan) != required or plan["action"] != "deploy":
88
83
  raise DeploymentKitError("Malformed application deployment plan.")
89
84
 
90
- current = app_inventory(plan["targetName"])
91
- if _inventory_snapshot(current) != plan["inventorySnapshot"]:
92
- raise DeploymentKitError(
93
- "Application inventory changed after the plan was reviewed; create and authorize a fresh plan."
94
- )
95
- platform = platform_status(plan["targetName"])
96
- if platform["state"] != "ready":
97
- raise DeploymentKitError("Shared host platform is no longer ready; application deployment is blocked.")
98
85
 
99
- transfer_image(plan["targetName"], plan["image"])
86
+ def _validate_transfer(plan: dict[str, Any], transfer: dict[str, Any]) -> str:
87
+ required = {
88
+ "targetName",
89
+ "image",
90
+ "registryDigest",
91
+ "localImageId",
92
+ "remoteImageId",
93
+ "runtimeImage",
94
+ "status",
95
+ "mutated",
96
+ }
97
+ if not isinstance(transfer, dict) or set(transfer) != required:
98
+ raise DeploymentKitError("Malformed workstation image-transfer evidence.")
99
+ if transfer["targetName"] != plan["targetName"]:
100
+ raise DeploymentKitError("Image-transfer target does not match the deployment plan.")
101
+ if transfer["registryDigest"] != plan["image"] or transfer["image"] != plan["image"]:
102
+ raise DeploymentKitError("Image-transfer registry digest does not match the deployment plan.")
103
+ local_id = _runtime_image_id(transfer["localImageId"])
104
+ remote_id = _runtime_image_id(transfer["remoteImageId"])
105
+ runtime_id = _runtime_image_id(transfer["runtimeImage"])
106
+ if not (local_id == remote_id == runtime_id):
107
+ raise DeploymentKitError("Workstation and remote Docker image IDs do not match.")
108
+ if transfer["status"] not in {"already-present", "transferred"} or not isinstance(transfer["mutated"], bool):
109
+ raise DeploymentKitError("Image-transfer evidence has an invalid terminal state.")
110
+ return runtime_id
111
+
112
+
113
+ def _run_preloaded_deploy(
114
+ profile: dict[str, Any],
115
+ address: str,
116
+ spec: dict[str, Any],
117
+ expected_spec_sha: str,
118
+ runtime_image_id: str,
119
+ ) -> None:
120
+ command = ssh_args(
121
+ profile,
122
+ address,
123
+ command="sudo -n bash -s -- deploy " + (expected_spec_sha or "NONE") + " " + profile["user"],
124
+ )
125
+ completed = subprocess.run(
126
+ command,
127
+ input=_preloaded_script(spec, runtime_image_id).encode("utf-8"),
128
+ check=False,
129
+ capture_output=True,
130
+ )
131
+ if completed.returncode != 0:
132
+ stdout = completed.stdout.decode("utf-8", errors="replace")
133
+ stderr = completed.stderr.decode("utf-8", errors="replace")
134
+ detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
135
+ raise DeploymentKitError(f"Workstation-mediated remote application deployment failed: {detail}")
136
+
137
+
138
+ def apply_app_deploy_with_preloaded_image(
139
+ plan: dict[str, Any], transfer: dict[str, Any]
140
+ ) -> dict[str, Any]:
141
+ """Apply a reviewed deployment plan using already-verified transfer evidence."""
142
+
143
+ _validate_plan(plan)
144
+ runtime_image_id = _validate_transfer(plan, transfer)
100
145
 
101
- # Image transfer can take time. Re-check the material app/platform evidence before
102
- # changing runtime state; a changed plan must never ride through on stale approval.
103
146
  current = app_inventory(plan["targetName"])
104
147
  if _inventory_snapshot(current) != plan["inventorySnapshot"]:
105
148
  raise DeploymentKitError(
@@ -116,6 +159,7 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
116
159
  evidence["address"],
117
160
  plan["spec"],
118
161
  plan["expectedSpecSha256"] or "",
162
+ runtime_image_id,
119
163
  )
120
164
 
121
165
  final = app_inventory(plan["targetName"])
@@ -132,5 +176,22 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
132
176
  raise DeploymentKitError("Application deployment completed but runtime readiness is not healthy.")
133
177
  final["mutated"] = True
134
178
  final["imageSource"] = "workstation-transfer"
179
+ final["imageTransfer"] = transfer
135
180
  final["attachesSharedNetwork"] = NETWORK_NAME
136
181
  return final
182
+
183
+
184
+ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
185
+ _validate_plan(plan)
186
+
187
+ current = app_inventory(plan["targetName"])
188
+ if _inventory_snapshot(current) != plan["inventorySnapshot"]:
189
+ raise DeploymentKitError(
190
+ "Application inventory changed after the plan was reviewed; create and authorize a fresh plan."
191
+ )
192
+ platform = platform_status(plan["targetName"])
193
+ if platform["state"] != "ready":
194
+ raise DeploymentKitError("Shared host platform is no longer ready; application deployment is blocked.")
195
+
196
+ transfer = transfer_image(plan["targetName"], plan["image"])
197
+ return apply_app_deploy_with_preloaded_image(plan, transfer)
@@ -1,4 +1,4 @@
1
- """CLI for portable developer environment definitions and Home Lab onboarding."""
1
+ """CLI for portable developer environment definitions and remote setup orchestration."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -15,6 +15,7 @@ from .environment_probe import (
15
15
  plan_environment_proof,
16
16
  run_environment_proof,
17
17
  )
18
+ from .environment_setup import remote_setup_plan, remote_setup_status
18
19
  from .errors import DeploymentKitError
19
20
  from .home_lab import HOME_LAB_BOOTSTRAP_PHRASE, apply_home_lab_bootstrap, plan_home_lab_bootstrap
20
21
  from .image_transfer import transfer_image, verify_local_image
@@ -59,6 +60,25 @@ def build_parser() -> argparse.ArgumentParser:
59
60
  parser = argparse.ArgumentParser(prog="project-forge-deploy environment")
60
61
  groups = parser.add_subparsers(dest="group", required=True)
61
62
 
63
+ setup = groups.add_parser(
64
+ "setup",
65
+ help="Read-only remote setup status/plan orchestration across target access, host platform, and proof readiness",
66
+ )
67
+ setup_sub = setup.add_subparsers(dest="command", required=True)
68
+ setup_status = setup_sub.add_parser("status", help="Show one terminal readiness model for a remote target")
69
+ setup_status.add_argument("name")
70
+ setup_status.add_argument("--json", action="store_true")
71
+ setup_plan = setup_sub.add_parser("plan", help="Show the exact next deterministic remote setup action")
72
+ setup_plan.add_argument("name")
73
+ setup_plan.add_argument("--owner-repo")
74
+ setup_plan.add_argument("--environment")
75
+ setup_plan.add_argument(
76
+ "--production",
77
+ action="store_true",
78
+ help="Classify the optional proof environment as Production",
79
+ )
80
+ setup_plan.add_argument("--json", action="store_true")
81
+
62
82
  profile = groups.add_parser("profile", help="Personal/portable non-secret target profile operations")
63
83
  profile_sub = profile.add_subparsers(dest="command", required=True)
64
84
 
@@ -178,6 +198,20 @@ def _home_lab_plan(args: argparse.Namespace) -> dict[str, Any]:
178
198
  def main(argv: list[str] | None = None) -> int:
179
199
  args = build_parser().parse_args(argv)
180
200
  try:
201
+ if args.group == "setup" and args.command == "status":
202
+ _print(remote_setup_status(args.name), as_json=args.json)
203
+ return 0
204
+ if args.group == "setup" and args.command == "plan":
205
+ _print(
206
+ remote_setup_plan(
207
+ args.name,
208
+ owner_repo=args.owner_repo,
209
+ environment=args.environment,
210
+ production=args.production,
211
+ ),
212
+ as_json=args.json,
213
+ )
214
+ return 0
181
215
  if args.group == "profile" and args.command == "status":
182
216
  _print(repository_status(), as_json=args.json)
183
217
  return 0
@@ -243,16 +277,19 @@ def main(argv: list[str] | None = None) -> int:
243
277
  production=args.production,
244
278
  probe_image=args.probe_image,
245
279
  )
246
- _print(plan, as_json=args.json if args.command == "plan" else False)
247
280
  if args.command == "plan":
281
+ _print(plan, as_json=args.json)
248
282
  return 0
283
+ if not args.json:
284
+ _print(plan, as_json=False)
249
285
  _confirm(
250
286
  args,
251
287
  plan["requiredAuthorization"],
252
288
  f"run a disposable deployment proof for {plan['environment']} on target {plan['targetName']}",
253
289
  )
254
- _print(run_environment_proof(plan), as_json=args.json)
255
- return 0
290
+ result = run_environment_proof(plan)
291
+ _print(result, as_json=args.json)
292
+ return 0 if result.get("proof") == "passed" else 2
256
293
  raise DeploymentKitError("Unsupported environment command.")
257
294
  except DeploymentKitError as error:
258
295
  print(f"ERROR: {error}", file=sys.stderr)