@openrig/cli 0.3.4 → 0.4.1
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/daemon/assets/plugins/openrig-core/hooks/claude.json +11 -0
- package/daemon/assets/plugins/openrig-core/hooks/codex.json +14 -3
- package/daemon/assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs +34 -0
- package/daemon/assets/plugins/openrig-core/hooks/scripts/compaction-restore-bridge.cjs +28 -24
- package/daemon/assets/plugins/openrig-core/skills/claude-compaction-restore/scripts/precompact-hook.mjs +72 -11
- package/daemon/assets/plugins/openrig-core/skills/queue-handoff/SKILL.md +62 -29
- package/daemon/dist/adapters/codex-resume.d.ts +2 -1
- package/daemon/dist/adapters/codex-resume.d.ts.map +1 -1
- package/daemon/dist/adapters/codex-resume.js +18 -6
- package/daemon/dist/adapters/codex-resume.js.map +1 -1
- package/daemon/dist/adapters/codex-runtime-adapter.d.ts +37 -1
- package/daemon/dist/adapters/codex-runtime-adapter.d.ts.map +1 -1
- package/daemon/dist/adapters/codex-runtime-adapter.js +212 -6
- package/daemon/dist/adapters/codex-runtime-adapter.js.map +1 -1
- package/daemon/dist/adapters/tmux.d.ts +28 -14
- package/daemon/dist/adapters/tmux.d.ts.map +1 -1
- package/daemon/dist/adapters/tmux.js +65 -30
- package/daemon/dist/adapters/tmux.js.map +1 -1
- package/daemon/dist/db/migrations/043_resume_provenance.d.ts +3 -0
- package/daemon/dist/db/migrations/043_resume_provenance.d.ts.map +1 -0
- package/daemon/dist/db/migrations/043_resume_provenance.js +5 -0
- package/daemon/dist/db/migrations/043_resume_provenance.js.map +1 -0
- package/daemon/dist/db/migrations/044_queue_item_summary.d.ts +21 -0
- package/daemon/dist/db/migrations/044_queue_item_summary.d.ts.map +1 -0
- package/daemon/dist/db/migrations/044_queue_item_summary.js +25 -0
- package/daemon/dist/db/migrations/044_queue_item_summary.js.map +1 -0
- package/daemon/dist/domain/agent-activity-store.d.ts +11 -1
- package/daemon/dist/domain/agent-activity-store.d.ts.map +1 -1
- package/daemon/dist/domain/agent-activity-store.js +22 -2
- package/daemon/dist/domain/agent-activity-store.js.map +1 -1
- package/daemon/dist/domain/claude-compaction-enforcer.d.ts.map +1 -1
- package/daemon/dist/domain/claude-compaction-enforcer.js +69 -1
- package/daemon/dist/domain/claude-compaction-enforcer.js.map +1 -1
- package/daemon/dist/domain/daemon-version.d.ts +15 -0
- package/daemon/dist/domain/daemon-version.d.ts.map +1 -0
- package/daemon/dist/domain/daemon-version.js +28 -0
- package/daemon/dist/domain/daemon-version.js.map +1 -0
- package/daemon/dist/domain/native-resume-probe.d.ts +2 -1
- package/daemon/dist/domain/native-resume-probe.d.ts.map +1 -1
- package/daemon/dist/domain/native-resume-probe.js +21 -2
- package/daemon/dist/domain/native-resume-probe.js.map +1 -1
- package/daemon/dist/domain/node-cmux-service.d.ts +1 -0
- package/daemon/dist/domain/node-cmux-service.d.ts.map +1 -1
- package/daemon/dist/domain/node-cmux-service.js +29 -2
- package/daemon/dist/domain/node-cmux-service.js.map +1 -1
- package/daemon/dist/domain/node-inventory.d.ts.map +1 -1
- package/daemon/dist/domain/node-inventory.js +70 -46
- package/daemon/dist/domain/node-inventory.js.map +1 -1
- package/daemon/dist/domain/node-launcher.d.ts +7 -7
- package/daemon/dist/domain/node-launcher.d.ts.map +1 -1
- package/daemon/dist/domain/node-launcher.js +0 -22
- package/daemon/dist/domain/node-launcher.js.map +1 -1
- package/daemon/dist/domain/profile-resolver.d.ts +4 -5
- package/daemon/dist/domain/profile-resolver.d.ts.map +1 -1
- package/daemon/dist/domain/profile-resolver.js.map +1 -1
- package/daemon/dist/domain/queue-repository.d.ts +22 -0
- package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
- package/daemon/dist/domain/queue-repository.js +39 -2
- package/daemon/dist/domain/queue-repository.js.map +1 -1
- package/daemon/dist/domain/restore-check-service.d.ts +14 -0
- package/daemon/dist/domain/restore-check-service.d.ts.map +1 -1
- package/daemon/dist/domain/restore-check-service.js +78 -8
- package/daemon/dist/domain/restore-check-service.js.map +1 -1
- package/daemon/dist/domain/restore-orchestrator.d.ts.map +1 -1
- package/daemon/dist/domain/restore-orchestrator.js +3 -3
- package/daemon/dist/domain/restore-orchestrator.js.map +1 -1
- package/daemon/dist/domain/resume-metadata-refresher.js +1 -1
- package/daemon/dist/domain/resume-metadata-refresher.js.map +1 -1
- package/daemon/dist/domain/resume-token-validation.d.ts +17 -0
- package/daemon/dist/domain/resume-token-validation.d.ts.map +1 -0
- package/daemon/dist/domain/resume-token-validation.js +46 -0
- package/daemon/dist/domain/resume-token-validation.js.map +1 -0
- package/daemon/dist/domain/rigspec-instantiator.d.ts.map +1 -1
- package/daemon/dist/domain/rigspec-instantiator.js +3 -6
- package/daemon/dist/domain/rigspec-instantiator.js.map +1 -1
- package/daemon/dist/domain/runtime-adapter.d.ts.map +1 -1
- package/daemon/dist/domain/runtime-adapter.js +1 -0
- package/daemon/dist/domain/runtime-adapter.js.map +1 -1
- package/daemon/dist/domain/scope/dot-id.d.ts +43 -0
- package/daemon/dist/domain/scope/dot-id.d.ts.map +1 -0
- package/daemon/dist/domain/scope/dot-id.js +165 -0
- package/daemon/dist/domain/scope/dot-id.js.map +1 -0
- package/daemon/dist/domain/scope/scope-audit.d.ts +38 -0
- package/daemon/dist/domain/scope/scope-audit.d.ts.map +1 -0
- package/daemon/dist/domain/scope/scope-audit.js +191 -0
- package/daemon/dist/domain/scope/scope-audit.js.map +1 -0
- package/daemon/dist/domain/scope/types.d.ts +67 -0
- package/daemon/dist/domain/scope/types.d.ts.map +1 -0
- package/daemon/dist/domain/scope/types.js +46 -0
- package/daemon/dist/domain/scope/types.js.map +1 -0
- package/daemon/dist/domain/seat-attention-reconciler.d.ts +12 -0
- package/daemon/dist/domain/seat-attention-reconciler.d.ts.map +1 -1
- package/daemon/dist/domain/seat-attention-reconciler.js +116 -88
- package/daemon/dist/domain/seat-attention-reconciler.js.map +1 -1
- package/daemon/dist/domain/session-registry.d.ts +15 -1
- package/daemon/dist/domain/session-registry.d.ts.map +1 -1
- package/daemon/dist/domain/session-registry.js +46 -4
- package/daemon/dist/domain/session-registry.js.map +1 -1
- package/daemon/dist/domain/session-transport.d.ts +10 -0
- package/daemon/dist/domain/session-transport.d.ts.map +1 -1
- package/daemon/dist/domain/session-transport.js +153 -19
- package/daemon/dist/domain/session-transport.js.map +1 -1
- package/daemon/dist/domain/skill-audit.d.ts +47 -0
- package/daemon/dist/domain/skill-audit.d.ts.map +1 -0
- package/daemon/dist/domain/skill-audit.js +218 -0
- package/daemon/dist/domain/skill-audit.js.map +1 -0
- package/daemon/dist/domain/skill-discovery.d.ts +15 -0
- package/daemon/dist/domain/skill-discovery.d.ts.map +1 -1
- package/daemon/dist/domain/skill-discovery.js +70 -0
- package/daemon/dist/domain/skill-discovery.js.map +1 -1
- package/daemon/dist/domain/skill-mirror-drift.d.ts +10 -0
- package/daemon/dist/domain/skill-mirror-drift.d.ts.map +1 -0
- package/daemon/dist/domain/skill-mirror-drift.js +24 -0
- package/daemon/dist/domain/skill-mirror-drift.js.map +1 -0
- package/daemon/dist/domain/slices/slice-detail-projector.d.ts.map +1 -1
- package/daemon/dist/domain/slices/slice-detail-projector.js +10 -3
- package/daemon/dist/domain/slices/slice-detail-projector.js.map +1 -1
- package/daemon/dist/domain/startup-orchestrator.js +1 -1
- package/daemon/dist/domain/startup-orchestrator.js.map +1 -1
- package/daemon/dist/domain/types.d.ts +48 -14
- package/daemon/dist/domain/types.d.ts.map +1 -1
- package/daemon/dist/domain/types.js.map +1 -1
- package/daemon/dist/domain/user-settings/settings-store.d.ts +1 -1
- package/daemon/dist/domain/user-settings/settings-store.d.ts.map +1 -1
- package/daemon/dist/domain/user-settings/settings-store.js +4 -0
- package/daemon/dist/domain/user-settings/settings-store.js.map +1 -1
- package/daemon/dist/domain/whoami-service.d.ts +1 -0
- package/daemon/dist/domain/whoami-service.d.ts.map +1 -1
- package/daemon/dist/domain/whoami-service.js +8 -4
- package/daemon/dist/domain/whoami-service.js.map +1 -1
- package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts +1 -0
- package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts.map +1 -1
- package/daemon/dist/domain/workspace/default-workspace-scaffold.js +25 -1
- package/daemon/dist/domain/workspace/default-workspace-scaffold.js.map +1 -1
- package/daemon/dist/index.d.ts.map +1 -1
- package/daemon/dist/index.js +25 -3
- package/daemon/dist/index.js.map +1 -1
- package/daemon/dist/routes/activity.d.ts.map +1 -1
- package/daemon/dist/routes/activity.js +29 -0
- package/daemon/dist/routes/activity.js.map +1 -1
- package/daemon/dist/routes/bundles.d.ts.map +1 -1
- package/daemon/dist/routes/bundles.js +1 -18
- package/daemon/dist/routes/bundles.js.map +1 -1
- package/daemon/dist/routes/health-summary.d.ts.map +1 -1
- package/daemon/dist/routes/health-summary.js +13 -2
- package/daemon/dist/routes/health-summary.js.map +1 -1
- package/daemon/dist/routes/queue.d.ts.map +1 -1
- package/daemon/dist/routes/queue.js +11 -0
- package/daemon/dist/routes/queue.js.map +1 -1
- package/daemon/dist/routes/restore-check.js +33 -2
- package/daemon/dist/routes/restore-check.js.map +1 -1
- package/daemon/dist/routes/scope-audit.d.ts +3 -0
- package/daemon/dist/routes/scope-audit.d.ts.map +1 -0
- package/daemon/dist/routes/scope-audit.js +175 -0
- package/daemon/dist/routes/scope-audit.js.map +1 -0
- package/daemon/dist/routes/sessions.d.ts.map +1 -1
- package/daemon/dist/routes/sessions.js +67 -3
- package/daemon/dist/routes/sessions.js.map +1 -1
- package/daemon/dist/routes/skills.d.ts.map +1 -1
- package/daemon/dist/routes/skills.js +43 -0
- package/daemon/dist/routes/skills.js.map +1 -1
- package/daemon/dist/routes/terminal-ws.d.ts +7 -0
- package/daemon/dist/routes/terminal-ws.d.ts.map +1 -0
- package/daemon/dist/routes/terminal-ws.js +132 -0
- package/daemon/dist/routes/terminal-ws.js.map +1 -0
- package/daemon/dist/routes/transport.d.ts +3 -1
- package/daemon/dist/routes/transport.d.ts.map +1 -1
- package/daemon/dist/routes/transport.js +26 -1
- package/daemon/dist/routes/transport.js.map +1 -1
- package/daemon/dist/routes/whoami.d.ts.map +1 -1
- package/daemon/dist/routes/whoami.js +5 -0
- package/daemon/dist/routes/whoami.js.map +1 -1
- package/daemon/dist/server.d.ts +6 -0
- package/daemon/dist/server.d.ts.map +1 -1
- package/daemon/dist/server.js +29 -2
- package/daemon/dist/server.js.map +1 -1
- package/daemon/dist/startup.d.ts +8 -0
- package/daemon/dist/startup.d.ts.map +1 -1
- package/daemon/dist/startup.js +41 -10
- package/daemon/dist/startup.js.map +1 -1
- package/daemon/dist/terminal/TerminalSessionBroker.d.ts +173 -0
- package/daemon/dist/terminal/TerminalSessionBroker.d.ts.map +1 -0
- package/daemon/dist/terminal/TerminalSessionBroker.js +570 -0
- package/daemon/dist/terminal/TerminalSessionBroker.js.map +1 -0
- package/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md +131 -2
- package/daemon/specs/agents/shared/skills/pods/orchestration-team/SKILL.md +29 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +23 -0
- package/dist/client.js.map +1 -1
- package/dist/commands/auth.d.ts +11 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +145 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +6 -6
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/config-init-workspace.d.ts.map +1 -1
- package/dist/commands/config-init-workspace.js +7 -4
- package/dist/commands/config-init-workspace.js.map +1 -1
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +41 -5
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +27 -0
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +34 -0
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +361 -53
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/queue.d.ts.map +1 -1
- package/dist/commands/queue.js +83 -3
- package/dist/commands/queue.js.map +1 -1
- package/dist/commands/restore-check.d.ts.map +1 -1
- package/dist/commands/restore-check.js +80 -2
- package/dist/commands/restore-check.js.map +1 -1
- package/dist/commands/scope.d.ts.map +1 -1
- package/dist/commands/scope.js +747 -5
- package/dist/commands/scope.js.map +1 -1
- package/dist/commands/seat.d.ts +3 -1
- package/dist/commands/seat.d.ts.map +1 -1
- package/dist/commands/seat.js +61 -1
- package/dist/commands/seat.js.map +1 -1
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +46 -13
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/skill.d.ts +4 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +77 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +27 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +208 -20
- package/dist/commands/whoami.js.map +1 -1
- package/dist/cross-host-executor.d.ts +1 -1
- package/dist/cross-host-executor.d.ts.map +1 -1
- package/dist/cross-host-executor.js +3 -2
- package/dist/cross-host-executor.js.map +1 -1
- package/dist/cross-host-types.d.ts +2 -0
- package/dist/cross-host-types.d.ts.map +1 -0
- package/dist/cross-host-types.js +2 -0
- package/dist/cross-host-types.js.map +1 -0
- package/dist/host-registry.d.ts +27 -5
- package/dist/host-registry.d.ts.map +1 -1
- package/dist/host-registry.js +86 -19
- package/dist/host-registry.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/codex-auth.d.ts +124 -0
- package/dist/lib/codex-auth.d.ts.map +1 -0
- package/dist/lib/codex-auth.js +418 -0
- package/dist/lib/codex-auth.js.map +1 -0
- package/dist/lib/scope/progress-edit.d.ts +42 -0
- package/dist/lib/scope/progress-edit.d.ts.map +1 -0
- package/dist/lib/scope/progress-edit.js +171 -0
- package/dist/lib/scope/progress-edit.js.map +1 -0
- package/dist/lib/scope/scope-audit.d.ts +38 -0
- package/dist/lib/scope/scope-audit.d.ts.map +1 -0
- package/dist/lib/scope/scope-audit.js +191 -0
- package/dist/lib/scope/scope-audit.js.map +1 -0
- package/dist/lib/scope/templates.d.ts +11 -0
- package/dist/lib/scope/templates.d.ts.map +1 -1
- package/dist/lib/scope/templates.js +22 -1
- package/dist/lib/scope/templates.js.map +1 -1
- package/dist/lib/scope/trust.d.ts +30 -0
- package/dist/lib/scope/trust.d.ts.map +1 -0
- package/dist/lib/scope/trust.js +80 -0
- package/dist/lib/scope/trust.js.map +1 -0
- package/dist/lib/scope/types.d.ts +6 -0
- package/dist/lib/scope/types.d.ts.map +1 -1
- package/dist/lib/scope/types.js +8 -0
- package/dist/lib/scope/types.js.map +1 -1
- package/dist/lib/scope-templates/backlog-deprecation.md +1 -0
- package/dist/lib/scope-templates/backlog-tech-debt.md +1 -0
- package/dist/lib/scope-templates/bug-fix.md +1 -0
- package/dist/lib/scope-templates/mission-brief.md +20 -0
- package/dist/lib/scope-templates/mission-placeholder.md +1 -0
- package/dist/lib/scope-templates/mission-progress.md +12 -0
- package/dist/lib/scope-templates/mission-release.md +1 -0
- package/dist/lib/scope-templates/placeholder.md +9 -0
- package/dist/lib/scope-templates/proof.md +17 -0
- package/dist/lib/scope-templates/release-feature.md +1 -0
- package/dist/lib/scope-templates/research.md +1 -0
- package/dist/lib/scope-templates/slice-progress.md +11 -0
- package/dist/remote-host-ops.d.ts +24 -0
- package/dist/remote-host-ops.d.ts.map +1 -0
- package/dist/remote-host-ops.js +53 -0
- package/dist/remote-host-ops.js.map +1 -0
- package/package.json +2 -1
- package/ui/dist/assets/index-CnqRX7A3.css +32 -0
- package/ui/dist/assets/index-CtL9jXFt.js +582 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-C_Q5FczU.css +0 -1
- package/ui/dist/assets/index-De6vv0yO.js +0 -552
package/dist/commands/scope.js
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
-
import { CLOSE_REASONS, MISSION_TEMPLATE_KINDS, SLICE_TEMPLATE_KINDS, ScopeCliError, } from "../lib/scope/types.js";
|
|
11
|
+
import { CLOSE_REASONS, MISSION_TEMPLATE_KINDS, SLICE_TEMPLATE_KINDS, STAGE_VALUES, ScopeCliError, } from "../lib/scope/types.js";
|
|
12
12
|
import { DEFAULT_PROJECT_PREFIX, inferMissionDotId, isMissionDotId, nextEscapeBandOrdinal, sliceIdFromMission, } from "../lib/scope/dot-id.js";
|
|
13
|
-
import { ensureMissionIdPersisted, findMission, findSlice, listMissions, listSlices, moveSlice, nextSliceNN, pad2, resolveMissionsRoot, splitFrontmatter, todayDateISO, updateFrontmatter, } from "../lib/scope/scope-fs.js";
|
|
14
|
-
import { renderMissionNotesTemplate, renderMissionTemplate, renderSliceTemplate, titleFromSlug, } from "../lib/scope/templates.js";
|
|
13
|
+
import { ensureMissionId, ensureMissionIdPersisted, findMission, findSlice, listMissions, listSlices, moveSlice, nextSliceNN, pad2, readFrontmatter, resolveMissionsRoot, splitFrontmatter, todayDateISO, updateFrontmatter, } from "../lib/scope/scope-fs.js";
|
|
14
|
+
import { renderMissionBriefTemplate, renderMissionNotesTemplate, renderMissionProgressTemplate, renderMissionTemplate, renderSliceProofTemplate, renderSliceProgressTemplate, renderSliceTemplate, titleFromSlug, } from "../lib/scope/templates.js";
|
|
15
|
+
import { addProgressRow, DEFAULT_PROGRESS_SECTION, parseStatus, PROGRESS_STATUSES, setProgressRow, } from "../lib/scope/progress-edit.js";
|
|
16
|
+
import { deriveScopeTrust } from "../lib/scope/trust.js";
|
|
15
17
|
function makeStdout() {
|
|
16
18
|
return { write: (text) => process.stdout.write(text) };
|
|
17
19
|
}
|
|
@@ -63,6 +65,16 @@ function getOpts(cmd) {
|
|
|
63
65
|
}
|
|
64
66
|
return {};
|
|
65
67
|
}
|
|
68
|
+
/** FR-5: a one-line human render of the derived stage. Shows the declared
|
|
69
|
+
* stage, and (when a weak `verified` downgrades it) the effective stage +
|
|
70
|
+
* why. Derived at read time; nothing is written. */
|
|
71
|
+
function formatTrustLine(trust) {
|
|
72
|
+
const declared = trust.declaredStage || "—";
|
|
73
|
+
if (trust.downgraded) {
|
|
74
|
+
return ` stage: ${declared} (effective: ${trust.effectiveStage} — ${trust.verified.status})\n`;
|
|
75
|
+
}
|
|
76
|
+
return ` stage: ${declared}\n`;
|
|
77
|
+
}
|
|
66
78
|
// ---------------------------------------------------------------------
|
|
67
79
|
// rig scope slice ls
|
|
68
80
|
// ---------------------------------------------------------------------
|
|
@@ -131,6 +143,8 @@ function buildSliceShowCommand() {
|
|
|
131
143
|
const readme = slice.readmePath ? fs.readFileSync(slice.readmePath, "utf8") : null;
|
|
132
144
|
const children = fs.readdirSync(slice.absPath, { withFileTypes: true })
|
|
133
145
|
.map((e) => ({ name: e.name, kind: e.isDirectory() ? "dir" : "file" }));
|
|
146
|
+
// FR-5: derive read-time trust from (stage x verified) — NEVER stored.
|
|
147
|
+
const trust = deriveScopeTrust(slice.frontmatter);
|
|
134
148
|
const payload = {
|
|
135
149
|
ok: true,
|
|
136
150
|
slice: {
|
|
@@ -140,6 +154,7 @@ function buildSliceShowCommand() {
|
|
|
140
154
|
status: slice.status,
|
|
141
155
|
path: slice.absPath,
|
|
142
156
|
frontmatter: slice.frontmatter,
|
|
157
|
+
trust,
|
|
143
158
|
readme,
|
|
144
159
|
children,
|
|
145
160
|
},
|
|
@@ -151,6 +166,7 @@ function buildSliceShowCommand() {
|
|
|
151
166
|
out.write(`Slice: ${slice.missionName}/${slice.name}\n`);
|
|
152
167
|
out.write(` id: ${slice.id ?? "—"}\n`);
|
|
153
168
|
out.write(` status: ${slice.status ?? "—"}\n`);
|
|
169
|
+
out.write(formatTrustLine(trust));
|
|
154
170
|
out.write(` path: ${slice.absPath}\n`);
|
|
155
171
|
out.write(` children: ${children.length}\n`);
|
|
156
172
|
if (readme) {
|
|
@@ -176,6 +192,7 @@ function buildSliceCreateCommand() {
|
|
|
176
192
|
.argument("<slug>", "Short slug (becomes the folder name's suffix)")
|
|
177
193
|
.option("--template <kind>", `Template: ${SLICE_TEMPLATE_KINDS.join(" | ")}`, "placeholder")
|
|
178
194
|
.option("--title <text>", "Display title (defaults to titlecased slug)")
|
|
195
|
+
.option("--readme-only", "Write progress_rail: readme-only in README frontmatter instead of scaffolding PROGRESS.md")
|
|
179
196
|
.option("--json", "Machine-readable output")
|
|
180
197
|
.action(async (missionName, rawSlug, opts, command) => {
|
|
181
198
|
const out = makeStdout();
|
|
@@ -216,17 +233,30 @@ function buildSliceCreateCommand() {
|
|
|
216
233
|
}
|
|
217
234
|
const id = sliceIdFromMission(missionId, nn);
|
|
218
235
|
const title = opts.title ?? titleFromSlug(slug);
|
|
236
|
+
const createdDate = todayDateISO();
|
|
219
237
|
const body = renderSliceTemplate(kind, {
|
|
220
238
|
id,
|
|
221
239
|
slice_number: pad2(nn),
|
|
222
240
|
slug,
|
|
223
241
|
mission: mission.name,
|
|
224
242
|
title,
|
|
225
|
-
created_date:
|
|
243
|
+
created_date: createdDate,
|
|
226
244
|
});
|
|
245
|
+
const proofBody = renderSliceProofTemplate({ id, title });
|
|
227
246
|
fs.mkdirSync(sliceAbs, { recursive: true });
|
|
247
|
+
fs.mkdirSync(path.join(sliceAbs, "proof"), { recursive: true });
|
|
228
248
|
const readmePath = path.join(sliceAbs, "README.md");
|
|
229
|
-
|
|
249
|
+
const readmeOnly = Boolean(opts.readmeOnly);
|
|
250
|
+
if (readmeOnly) {
|
|
251
|
+
const markerBody = body.replace(/^(---\n)/, `---\nprogress_rail: readme-only\n`);
|
|
252
|
+
fs.writeFileSync(readmePath, markerBody, "utf8");
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
fs.writeFileSync(readmePath, body, "utf8");
|
|
256
|
+
const progressPath = path.join(sliceAbs, "PROGRESS.md");
|
|
257
|
+
fs.writeFileSync(progressPath, renderSliceProgressTemplate(title), "utf8");
|
|
258
|
+
}
|
|
259
|
+
fs.writeFileSync(path.join(sliceAbs, "PROOF.md"), proofBody, "utf8");
|
|
230
260
|
const payload = {
|
|
231
261
|
ok: true,
|
|
232
262
|
slice: {
|
|
@@ -464,6 +494,8 @@ function buildMissionShowCommand() {
|
|
|
464
494
|
const slices = listSlices(mission, "all").map((s) => ({
|
|
465
495
|
name: s.name, id: s.id, status: s.status, nn: s.nn,
|
|
466
496
|
}));
|
|
497
|
+
// FR-5: derive read-time trust from (stage x verified) — NEVER stored.
|
|
498
|
+
const trust = deriveScopeTrust(mission.frontmatter);
|
|
467
499
|
const payload = {
|
|
468
500
|
ok: true,
|
|
469
501
|
mission: {
|
|
@@ -473,6 +505,7 @@ function buildMissionShowCommand() {
|
|
|
473
505
|
activeSliceCount: mission.activeSliceCount,
|
|
474
506
|
closedSliceCount: mission.closedSliceCount,
|
|
475
507
|
frontmatter: mission.frontmatter,
|
|
508
|
+
trust,
|
|
476
509
|
readme,
|
|
477
510
|
slices,
|
|
478
511
|
},
|
|
@@ -483,6 +516,7 @@ function buildMissionShowCommand() {
|
|
|
483
516
|
else {
|
|
484
517
|
out.write(`Mission: ${mission.name}\n`);
|
|
485
518
|
out.write(` id: ${mission.id ?? "—"}\n`);
|
|
519
|
+
out.write(formatTrustLine(trust));
|
|
486
520
|
out.write(` active slices: ${mission.activeSliceCount}\n`);
|
|
487
521
|
out.write(` closed slices: ${mission.closedSliceCount}\n`);
|
|
488
522
|
out.write(` path: ${mission.absPath}\n`);
|
|
@@ -593,11 +627,17 @@ function buildMissionCreateCommand() {
|
|
|
593
627
|
});
|
|
594
628
|
missionNotesRendered = { rendered: r.rendered, resolvedFrom: r.resolvedFrom };
|
|
595
629
|
}
|
|
630
|
+
const progressBody = renderMissionProgressTemplate(title);
|
|
631
|
+
const missionBriefBody = renderMissionBriefTemplate(title);
|
|
596
632
|
// All renders succeeded — safe to touch the filesystem.
|
|
597
633
|
fs.mkdirSync(absPath, { recursive: true });
|
|
598
634
|
fs.mkdirSync(path.join(absPath, "slices"), { recursive: true });
|
|
599
635
|
const readmePath = path.join(absPath, "README.md");
|
|
600
636
|
fs.writeFileSync(readmePath, readmeBody, "utf8");
|
|
637
|
+
const progressPath = path.join(absPath, "PROGRESS.md");
|
|
638
|
+
fs.writeFileSync(progressPath, progressBody, "utf8");
|
|
639
|
+
const missionBriefPath = path.join(absPath, "MISSION_BRIEF.md");
|
|
640
|
+
fs.writeFileSync(missionBriefPath, missionBriefBody, "utf8");
|
|
601
641
|
let missionNotesPath = null;
|
|
602
642
|
let missionNotesResolvedFrom = null;
|
|
603
643
|
if (missionNotesRendered) {
|
|
@@ -622,6 +662,7 @@ function buildMissionCreateCommand() {
|
|
|
622
662
|
template: templateKind,
|
|
623
663
|
path: absPath,
|
|
624
664
|
readmePath,
|
|
665
|
+
missionBriefPath,
|
|
625
666
|
missionNotesPath,
|
|
626
667
|
missionNotesResolvedFrom,
|
|
627
668
|
},
|
|
@@ -633,6 +674,698 @@ function buildMissionCreateCommand() {
|
|
|
633
674
|
});
|
|
634
675
|
}
|
|
635
676
|
// ---------------------------------------------------------------------
|
|
677
|
+
// Audit (B2 — read-only scope audit)
|
|
678
|
+
// ---------------------------------------------------------------------
|
|
679
|
+
function buildAuditCommand() {
|
|
680
|
+
return new Command("audit")
|
|
681
|
+
.description("Read-only scope audit: flag missing/malformed progress rails and registration ghosts")
|
|
682
|
+
.requiredOption("--mission <name>", "Mission to audit")
|
|
683
|
+
.option("--json", "Machine-readable JSON output")
|
|
684
|
+
.action(async (opts, command) => {
|
|
685
|
+
const out = makeStdout();
|
|
686
|
+
const json = Boolean(opts.json);
|
|
687
|
+
try {
|
|
688
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
689
|
+
const { classifyScopeItem } = await import("../lib/scope/scope-audit.js");
|
|
690
|
+
const missionName = opts.mission;
|
|
691
|
+
const missionDir = path.join(missionsRoot, missionName);
|
|
692
|
+
if (!fs.existsSync(missionDir)) {
|
|
693
|
+
throw new ScopeCliError({ fact: `Mission "${missionName}" not found at ${missionDir}.`, consequence: "Cannot audit.", action: "Check the mission name." });
|
|
694
|
+
}
|
|
695
|
+
const missionReadme = path.join(missionDir, "README.md");
|
|
696
|
+
const missionProgress = path.join(missionDir, "PROGRESS.md");
|
|
697
|
+
const missionBrief = path.join(missionDir, "MISSION_BRIEF.md");
|
|
698
|
+
const missionNotes = path.join(missionDir, "MISSION_NOTES.md");
|
|
699
|
+
const missionReadmeExists = fs.existsSync(missionReadme);
|
|
700
|
+
const missionProgressExists = fs.existsSync(missionProgress);
|
|
701
|
+
const missionBriefExists = fs.existsSync(missionBrief);
|
|
702
|
+
let missionResult;
|
|
703
|
+
if (!missionReadmeExists && missionProgressExists) {
|
|
704
|
+
missionResult = {
|
|
705
|
+
railStatus: "malformed",
|
|
706
|
+
findings: [{
|
|
707
|
+
kind: "orphan_progress",
|
|
708
|
+
severity: "high",
|
|
709
|
+
path: missionDir,
|
|
710
|
+
message: `PROGRESS.md exists but no README.md (orphan progress rail, no backing scope item)`,
|
|
711
|
+
remediation: `Add a README.md with frontmatter id, or remove the orphan PROGRESS.md`,
|
|
712
|
+
}],
|
|
713
|
+
frontmatterError: null,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
const missionFm = missionReadmeExists
|
|
718
|
+
? extractFrontmatterRaw(fs.readFileSync(missionReadme, "utf-8"))
|
|
719
|
+
: null;
|
|
720
|
+
missionResult = classifyScopeItem({
|
|
721
|
+
id: null,
|
|
722
|
+
path: missionDir,
|
|
723
|
+
readmeFrontmatterRaw: missionFm,
|
|
724
|
+
progressFileExists: missionProgressExists,
|
|
725
|
+
readmeOnlyMarker: false,
|
|
726
|
+
isActiveRelease: true,
|
|
727
|
+
level: "mission",
|
|
728
|
+
missionBriefExists,
|
|
729
|
+
missionBriefPath: missionBrief,
|
|
730
|
+
missionBriefContent: missionBriefExists ? fs.readFileSync(missionBrief, "utf-8") : null,
|
|
731
|
+
missionNotesExists: fs.existsSync(missionNotes),
|
|
732
|
+
missionNotesPath: missionNotes,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const slicesDir = path.join(missionDir, "slices");
|
|
736
|
+
const dogfoodEvidenceRoot = defaultDogfoodEvidenceRoot(missionsRoot);
|
|
737
|
+
const sliceResults = [];
|
|
738
|
+
if (fs.existsSync(slicesDir)) {
|
|
739
|
+
for (const entry of fs.readdirSync(slicesDir)) {
|
|
740
|
+
const sliceDir = path.join(slicesDir, entry);
|
|
741
|
+
if (!fs.statSync(sliceDir).isDirectory())
|
|
742
|
+
continue;
|
|
743
|
+
const sliceReadme = path.join(sliceDir, "README.md");
|
|
744
|
+
const sliceProgress = path.join(sliceDir, "PROGRESS.md");
|
|
745
|
+
const proofFile = path.join(sliceDir, "PROOF.md");
|
|
746
|
+
const proofDir = path.join(sliceDir, "proof");
|
|
747
|
+
if (!fs.existsSync(sliceReadme)) {
|
|
748
|
+
if (fs.existsSync(sliceProgress)) {
|
|
749
|
+
sliceResults.push({
|
|
750
|
+
name: entry,
|
|
751
|
+
result: {
|
|
752
|
+
railStatus: "malformed",
|
|
753
|
+
findings: [{
|
|
754
|
+
kind: "orphan_progress",
|
|
755
|
+
severity: "high",
|
|
756
|
+
path: sliceDir,
|
|
757
|
+
message: `PROGRESS.md exists but no README.md (orphan progress rail, no backing scope item)`,
|
|
758
|
+
remediation: `Add a README.md with frontmatter id, or remove the orphan PROGRESS.md`,
|
|
759
|
+
}],
|
|
760
|
+
frontmatterError: null,
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
const noReadmeResult = classifyScopeItem({
|
|
766
|
+
id: null,
|
|
767
|
+
path: sliceDir,
|
|
768
|
+
readmeFrontmatterRaw: null,
|
|
769
|
+
progressFileExists: false,
|
|
770
|
+
readmeOnlyMarker: false,
|
|
771
|
+
isActiveRelease: true,
|
|
772
|
+
level: "slice",
|
|
773
|
+
});
|
|
774
|
+
sliceResults.push({ name: entry, result: noReadmeResult });
|
|
775
|
+
}
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const sliceFm = extractFrontmatterRaw(fs.readFileSync(sliceReadme, "utf-8"));
|
|
779
|
+
const readmeOnlyMarker = sliceFm !== null && /^progress_rail\s*:\s*readme-only/m.test(sliceFm);
|
|
780
|
+
const sliceResult = classifyScopeItem({
|
|
781
|
+
id: null,
|
|
782
|
+
path: sliceDir,
|
|
783
|
+
readmeFrontmatterRaw: sliceFm,
|
|
784
|
+
progressFileExists: fs.existsSync(sliceProgress),
|
|
785
|
+
readmeOnlyMarker,
|
|
786
|
+
isActiveRelease: true,
|
|
787
|
+
level: "slice",
|
|
788
|
+
proofFileExists: fs.existsSync(proofFile),
|
|
789
|
+
proofFilePath: proofFile,
|
|
790
|
+
proofDirExists: fs.existsSync(proofDir),
|
|
791
|
+
proofDirPath: proofDir,
|
|
792
|
+
proofDirHasEntries: directoryHasEntries(proofDir),
|
|
793
|
+
hasProofPacket: hasProofPacketForSlice(dogfoodEvidenceRoot, entry),
|
|
794
|
+
});
|
|
795
|
+
if (!/^\d{2}-/.test(entry)) {
|
|
796
|
+
sliceResult.findings.push({
|
|
797
|
+
kind: "id_convention_violation",
|
|
798
|
+
severity: "high",
|
|
799
|
+
path: sliceDir,
|
|
800
|
+
message: `Directory "${entry}" does not match the NN-slug slice naming convention (e.g. 01-my-slice)`,
|
|
801
|
+
remediation: `Rename to NN-slug format or move out of slices/`,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
sliceResults.push({ name: entry, result: sliceResult });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const allFindings = [
|
|
808
|
+
...missionResult.findings.map((f) => ({ ...f, scope: "mission", scopeName: missionName })),
|
|
809
|
+
...sliceResults.flatMap((s) => s.result.findings.map((f) => ({ ...f, scope: "slice", scopeName: s.name }))),
|
|
810
|
+
];
|
|
811
|
+
const hardFindings = allFindings.filter((f) => f.severity === "high");
|
|
812
|
+
if (json) {
|
|
813
|
+
out.write(JSON.stringify({
|
|
814
|
+
ok: hardFindings.length === 0,
|
|
815
|
+
mission: { name: missionName, railStatus: missionResult.railStatus, frontmatterError: missionResult.frontmatterError, findings: missionResult.findings },
|
|
816
|
+
slices: sliceResults.map((s) => ({ name: s.name, railStatus: s.result.railStatus, frontmatterError: s.result.frontmatterError, findings: s.result.findings })),
|
|
817
|
+
totalFindings: allFindings.length,
|
|
818
|
+
}, null, 2));
|
|
819
|
+
out.write("\n");
|
|
820
|
+
if (hardFindings.length > 0)
|
|
821
|
+
process.exitCode = 1;
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
out.write(`Scope audit: ${missionName}\n`);
|
|
825
|
+
out.write(`Mission rail: ${missionResult.railStatus}\n`);
|
|
826
|
+
out.write(`Slices: ${sliceResults.length} total\n\n`);
|
|
827
|
+
if (allFindings.length > 0) {
|
|
828
|
+
out.write("FINDINGS:\n");
|
|
829
|
+
for (const f of allFindings) {
|
|
830
|
+
out.write(` [${f.severity}] [${f.kind}] ${f.scope}/${f.scopeName}\n`);
|
|
831
|
+
out.write(` ${f.message}\n`);
|
|
832
|
+
out.write(` fix: ${f.remediation}\n`);
|
|
833
|
+
}
|
|
834
|
+
if (hardFindings.length > 0) {
|
|
835
|
+
out.write(`\nFAIL: ${allFindings.length} finding(s)\n`);
|
|
836
|
+
process.exitCode = 1;
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
out.write(`\nWARN: ${allFindings.length} advisory finding(s)\n`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
out.write("PASS: all scope items have valid rails\n");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch (err) {
|
|
847
|
+
if (err instanceof ScopeCliError) {
|
|
848
|
+
fail(err, json, out);
|
|
849
|
+
}
|
|
850
|
+
throw err;
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function extractFrontmatterRaw(content) {
|
|
855
|
+
if (!content.startsWith("---"))
|
|
856
|
+
return null;
|
|
857
|
+
const match = /^---\s*\n([\s\S]*?)\n---/.exec(content);
|
|
858
|
+
return match ? match[1] : null;
|
|
859
|
+
}
|
|
860
|
+
function directoryHasEntries(dir) {
|
|
861
|
+
try {
|
|
862
|
+
return fs.readdirSync(dir).some((entry) => !entry.startsWith("."));
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function defaultDogfoodEvidenceRoot(missionsRoot) {
|
|
869
|
+
return path.join(path.dirname(missionsRoot), "dogfood-evidence");
|
|
870
|
+
}
|
|
871
|
+
function hasProofPacketForSlice(dogfoodEvidenceRoot, sliceName) {
|
|
872
|
+
let entries;
|
|
873
|
+
try {
|
|
874
|
+
entries = fs.readdirSync(dogfoodEvidenceRoot, { withFileTypes: true });
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
const sliceTokens = sliceName.split("-").filter((token) => token.length > 0 && !/^v\d+$/.test(token));
|
|
880
|
+
return entries.some((entry) => {
|
|
881
|
+
if (!entry.isDirectory())
|
|
882
|
+
return false;
|
|
883
|
+
const dirTokenSet = new Set(entry.name.split(/[-._]/).filter((token) => token.length > 0));
|
|
884
|
+
return sliceTokens.every((token) => dirTokenSet.has(token));
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
// ---------------------------------------------------------------------
|
|
888
|
+
// rig scope <tier> progress (OPR.0.4.0.33 FR-3 — deterministic update)
|
|
889
|
+
// ---------------------------------------------------------------------
|
|
890
|
+
/** Resolve which file a progress update edits for a scope dir: the
|
|
891
|
+
* PROGRESS.md when present, else the README's rail for a readme-only
|
|
892
|
+
* scope, else an error directing to create/repair (the verb UPDATES an
|
|
893
|
+
* existing surface; it does not scaffold). */
|
|
894
|
+
function resolveProgressTarget(scopeDir, level) {
|
|
895
|
+
const progressPath = path.join(scopeDir, "PROGRESS.md");
|
|
896
|
+
if (fs.existsSync(progressPath))
|
|
897
|
+
return { targetPath: progressPath, kind: "progress" };
|
|
898
|
+
const readmePath = path.join(scopeDir, "README.md");
|
|
899
|
+
if (fs.existsSync(readmePath)) {
|
|
900
|
+
const fm = readFrontmatter(readmePath);
|
|
901
|
+
if (String(fm.progress_rail ?? "") === "readme-only") {
|
|
902
|
+
return { targetPath: readmePath, kind: "readme-only" };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
throw new ScopeCliError({
|
|
906
|
+
fact: `${level} at ${scopeDir} has no progress surface (no PROGRESS.md and no readme-only rail).`,
|
|
907
|
+
consequence: "The progress verb updates an existing surface; it does not scaffold.",
|
|
908
|
+
action: `Backfill it with: rig scope ${level} repair <target> (creates PROGRESS.md), or rig scope ${level} create.`,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
/** Shared body for slice/mission progress: validate the mutually
|
|
912
|
+
* exclusive --add/--set modes, edit the resolved surface, write only
|
|
913
|
+
* on change. */
|
|
914
|
+
function runProgressUpdate(scopeDir, level, scopeName, opts, out, json) {
|
|
915
|
+
const hasAdd = typeof opts.add === "string";
|
|
916
|
+
const hasSet = typeof opts.set === "string";
|
|
917
|
+
if (hasAdd === hasSet) {
|
|
918
|
+
throw new ScopeCliError({
|
|
919
|
+
fact: hasAdd
|
|
920
|
+
? "Both --add and --set were given."
|
|
921
|
+
: "Neither --add nor --set was given.",
|
|
922
|
+
consequence: "No progress update was made.",
|
|
923
|
+
action: 'Pass exactly one of --add "<row text>" or --set "<row text>".',
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
const status = parseStatus(opts.status ?? "active");
|
|
927
|
+
const { targetPath, kind } = resolveProgressTarget(scopeDir, level);
|
|
928
|
+
const before = fs.readFileSync(targetPath, "utf8");
|
|
929
|
+
let result;
|
|
930
|
+
let operation;
|
|
931
|
+
if (hasAdd) {
|
|
932
|
+
operation = "add";
|
|
933
|
+
result = addProgressRow(before, {
|
|
934
|
+
section: opts.section ?? DEFAULT_PROGRESS_SECTION,
|
|
935
|
+
text: opts.add,
|
|
936
|
+
status,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
operation = "set";
|
|
941
|
+
result = setProgressRow(before, { text: opts.set, status });
|
|
942
|
+
}
|
|
943
|
+
if (result.changed)
|
|
944
|
+
fs.writeFileSync(targetPath, result.content, "utf8");
|
|
945
|
+
emit(out, {
|
|
946
|
+
ok: true,
|
|
947
|
+
progress: {
|
|
948
|
+
scope: level,
|
|
949
|
+
name: scopeName,
|
|
950
|
+
target: targetPath,
|
|
951
|
+
kind,
|
|
952
|
+
operation,
|
|
953
|
+
status,
|
|
954
|
+
changed: result.changed,
|
|
955
|
+
},
|
|
956
|
+
}, json, [
|
|
957
|
+
`${result.changed ? "Updated" : "No change"} ${level} ${scopeName} progress (${operation})`,
|
|
958
|
+
` target: ${targetPath}`,
|
|
959
|
+
` status: ${status}`,
|
|
960
|
+
]);
|
|
961
|
+
}
|
|
962
|
+
function buildSliceProgressCommand() {
|
|
963
|
+
return new Command("progress")
|
|
964
|
+
.description("Update a slice's progress rail deterministically (append a row, or set a row's status)")
|
|
965
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
966
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
967
|
+
.option("--add <text>", "Append a checkbox row with this text")
|
|
968
|
+
.option("--set <text>", "Set the status of the row whose trimmed text exactly matches")
|
|
969
|
+
.option("--section <heading>", `Section heading for --add (default: ${DEFAULT_PROGRESS_SECTION})`)
|
|
970
|
+
.option("--status <status>", `Row status: ${PROGRESS_STATUSES.join(" | ")}`, "active")
|
|
971
|
+
.option("--json", "Machine-readable output")
|
|
972
|
+
.action(async (slicePath, opts, command) => {
|
|
973
|
+
const out = makeStdout();
|
|
974
|
+
const json = Boolean(opts.json);
|
|
975
|
+
try {
|
|
976
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
977
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
978
|
+
runProgressUpdate(slice.absPath, "slice", slice.name, opts, out, json);
|
|
979
|
+
}
|
|
980
|
+
catch (err) {
|
|
981
|
+
fail(err, json, out);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
function buildMissionProgressCommand() {
|
|
986
|
+
return new Command("progress")
|
|
987
|
+
.description("Update a mission's progress rail deterministically (append a row, or set a row's status)")
|
|
988
|
+
.argument("<mission>", "Mission name")
|
|
989
|
+
.option("--add <text>", "Append a checkbox row with this text")
|
|
990
|
+
.option("--set <text>", "Set the status of the row whose trimmed text exactly matches")
|
|
991
|
+
.option("--section <heading>", `Section heading for --add (default: ${DEFAULT_PROGRESS_SECTION})`)
|
|
992
|
+
.option("--status <status>", `Row status: ${PROGRESS_STATUSES.join(" | ")}`, "active")
|
|
993
|
+
.option("--json", "Machine-readable output")
|
|
994
|
+
.action(async (missionName, opts, command) => {
|
|
995
|
+
const out = makeStdout();
|
|
996
|
+
const json = Boolean(opts.json);
|
|
997
|
+
try {
|
|
998
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
999
|
+
const mission = findMission(missionsRoot, missionName);
|
|
1000
|
+
runProgressUpdate(mission.absPath, "mission", mission.name, opts, out, json);
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
fail(err, json, out);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
/** Mirror the create-time title derivation so a backfilled PROGRESS.md
|
|
1008
|
+
* is byte-identical to what create would have written. */
|
|
1009
|
+
function backfillTitle(level, scopeDir) {
|
|
1010
|
+
const base = path.basename(scopeDir);
|
|
1011
|
+
return level === "mission"
|
|
1012
|
+
? titleFromSlug(base.replace(/^release-/, ""))
|
|
1013
|
+
: titleFromSlug(base.replace(/^\d+-/, ""));
|
|
1014
|
+
}
|
|
1015
|
+
/** Create a missing PROGRESS.md for a single scope dir. Idempotent
|
|
1016
|
+
* (skips when one exists) and non-clobbering (skips an intentional
|
|
1017
|
+
* readme-only scope, and skips README-less dirs that are not declared
|
|
1018
|
+
* scopes). */
|
|
1019
|
+
function backfillScopeProgress(scopeDir, level) {
|
|
1020
|
+
const name = path.basename(scopeDir);
|
|
1021
|
+
const readmePath = path.join(scopeDir, "README.md");
|
|
1022
|
+
if (!fs.existsSync(readmePath)) {
|
|
1023
|
+
return { scope: level, name, created: false, reason: "no-readme (not a declared scope)", path: null };
|
|
1024
|
+
}
|
|
1025
|
+
const progressPath = path.join(scopeDir, "PROGRESS.md");
|
|
1026
|
+
if (fs.existsSync(progressPath)) {
|
|
1027
|
+
return { scope: level, name, created: false, reason: "already-present", path: progressPath };
|
|
1028
|
+
}
|
|
1029
|
+
const fm = readFrontmatter(readmePath);
|
|
1030
|
+
if (String(fm.progress_rail ?? "") === "readme-only") {
|
|
1031
|
+
return { scope: level, name, created: false, reason: "readme-only (intentional opt-out)", path: null };
|
|
1032
|
+
}
|
|
1033
|
+
const title = backfillTitle(level, scopeDir);
|
|
1034
|
+
const body = level === "mission"
|
|
1035
|
+
? renderMissionProgressTemplate(title)
|
|
1036
|
+
: renderSliceProgressTemplate(title);
|
|
1037
|
+
fs.writeFileSync(progressPath, body, "utf8");
|
|
1038
|
+
return { scope: level, name, created: true, reason: "backfilled", path: progressPath };
|
|
1039
|
+
}
|
|
1040
|
+
// ---------------------------------------------------------------------
|
|
1041
|
+
// OPR.0.4.1.6 — stage + verified verbs (deterministic §2 maturity edits)
|
|
1042
|
+
// ---------------------------------------------------------------------
|
|
1043
|
+
/** Validate a stage against the §2 enum, rejecting invented values. */
|
|
1044
|
+
function validateStage(raw) {
|
|
1045
|
+
if (!STAGE_VALUES.includes(raw)) {
|
|
1046
|
+
throw new ScopeCliError({
|
|
1047
|
+
fact: `Invalid stage "${raw}".`,
|
|
1048
|
+
consequence: "Stage not changed.",
|
|
1049
|
+
action: `Use one of: ${STAGE_VALUES.join(" | ")}.`,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return raw;
|
|
1053
|
+
}
|
|
1054
|
+
/** Surgically set `stage` (+ `superseded-by` when superseded) on a scope
|
|
1055
|
+
* README, enforcing the §2 superseded-needs-successor rule. */
|
|
1056
|
+
function applyStage(readmePath, stage, successor) {
|
|
1057
|
+
const updates = { stage };
|
|
1058
|
+
if (stage === "superseded") {
|
|
1059
|
+
const id = typeof successor === "string" ? successor.trim() : "";
|
|
1060
|
+
if (!id) {
|
|
1061
|
+
throw new ScopeCliError({
|
|
1062
|
+
fact: "stage 'superseded' requires a successor.",
|
|
1063
|
+
consequence: "Stage not changed (a superseded scope must name what replaces it, per scope-and-versioning §2).",
|
|
1064
|
+
action: "Re-run with --successor <id>, e.g. --successor OPR.0.4.1.7.",
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
updates["superseded-by"] = id;
|
|
1068
|
+
}
|
|
1069
|
+
updateFrontmatter(readmePath, updates);
|
|
1070
|
+
}
|
|
1071
|
+
/** `retired` is an exit, not a rung — warn (do not block). */
|
|
1072
|
+
function warnRetired(stage) {
|
|
1073
|
+
if (stage === "retired") {
|
|
1074
|
+
process.stderr.write("Warning: stage 'retired' means do-not-use (an exit, not a maturity rung).\n");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/** Validate a --against provenance: mandatory, non-empty, non-whitespace
|
|
1078
|
+
* (the §2 "no bare timestamp" rule). Returns the trimmed source. */
|
|
1079
|
+
function validateAgainst(raw) {
|
|
1080
|
+
const source = typeof raw === "string" ? raw.trim() : "";
|
|
1081
|
+
if (!source) {
|
|
1082
|
+
throw new ScopeCliError({
|
|
1083
|
+
fact: "--against provenance is empty or missing.",
|
|
1084
|
+
consequence: "verified not stamped — scope-and-versioning §2 forbids a bare timestamp without a named source.",
|
|
1085
|
+
action: 'Provide what it was verified against, e.g. --against "runtime (npm+tag+origin)".',
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
return source;
|
|
1089
|
+
}
|
|
1090
|
+
function buildSliceStageCommand() {
|
|
1091
|
+
return new Command("stage")
|
|
1092
|
+
.description(`Set a slice's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
|
|
1093
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
1094
|
+
.argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
|
|
1095
|
+
.option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
|
|
1096
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
1097
|
+
.option("--json", "Machine-readable output")
|
|
1098
|
+
.action(async (slicePath, newStage, opts, command) => {
|
|
1099
|
+
const out = makeStdout();
|
|
1100
|
+
const json = Boolean(opts.json);
|
|
1101
|
+
try {
|
|
1102
|
+
const stage = validateStage(newStage);
|
|
1103
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1104
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
1105
|
+
if (!slice.readmePath) {
|
|
1106
|
+
throw new ScopeCliError({
|
|
1107
|
+
fact: `Slice ${slice.name} has no README.md.`,
|
|
1108
|
+
consequence: "Stage is a frontmatter field on the README; nothing to write.",
|
|
1109
|
+
action: "Create the slice README (rig scope slice create) before setting its stage.",
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
applyStage(slice.readmePath, stage, opts.successor);
|
|
1113
|
+
warnRetired(stage);
|
|
1114
|
+
const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
|
|
1115
|
+
emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
|
|
1116
|
+
`Set ${slice.missionName}/${slice.name} stage: ${stage}`,
|
|
1117
|
+
...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
|
|
1118
|
+
]);
|
|
1119
|
+
}
|
|
1120
|
+
catch (err) {
|
|
1121
|
+
fail(err, json, out);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function buildMissionStageCommand() {
|
|
1126
|
+
return new Command("stage")
|
|
1127
|
+
.description(`Set a mission's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
|
|
1128
|
+
.argument("<mission>", "Mission name")
|
|
1129
|
+
.argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
|
|
1130
|
+
.option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
|
|
1131
|
+
.option("--json", "Machine-readable output")
|
|
1132
|
+
.action(async (missionName, newStage, opts, command) => {
|
|
1133
|
+
const out = makeStdout();
|
|
1134
|
+
const json = Boolean(opts.json);
|
|
1135
|
+
try {
|
|
1136
|
+
const stage = validateStage(newStage);
|
|
1137
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1138
|
+
const mission = findMission(missionsRoot, missionName);
|
|
1139
|
+
if (!mission.readmePath) {
|
|
1140
|
+
throw new ScopeCliError({
|
|
1141
|
+
fact: `Mission ${mission.name} has no README.md.`,
|
|
1142
|
+
consequence: "Stage is a frontmatter field on the README; nothing to write.",
|
|
1143
|
+
action: "Create the mission README before setting its stage.",
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
applyStage(mission.readmePath, stage, opts.successor);
|
|
1147
|
+
warnRetired(stage);
|
|
1148
|
+
const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
|
|
1149
|
+
emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
|
|
1150
|
+
`Set ${mission.name} stage: ${stage}`,
|
|
1151
|
+
...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
|
|
1152
|
+
]);
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
fail(err, json, out);
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
function buildSliceVerifiedCommand() {
|
|
1160
|
+
return new Command("verified")
|
|
1161
|
+
.description("Stamp a slice's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
|
|
1162
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
1163
|
+
.option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
|
|
1164
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
1165
|
+
.option("--json", "Machine-readable output")
|
|
1166
|
+
.action(async (slicePath, opts, command) => {
|
|
1167
|
+
const out = makeStdout();
|
|
1168
|
+
const json = Boolean(opts.json);
|
|
1169
|
+
try {
|
|
1170
|
+
const source = validateAgainst(opts.against);
|
|
1171
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1172
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
1173
|
+
if (!slice.readmePath) {
|
|
1174
|
+
throw new ScopeCliError({
|
|
1175
|
+
fact: `Slice ${slice.name} has no README.md.`,
|
|
1176
|
+
consequence: "verified is a frontmatter field on the README; nothing to write.",
|
|
1177
|
+
action: "Create the slice README before stamping verified.",
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
const verified = `${todayDateISO()} against ${source}`;
|
|
1181
|
+
updateFrontmatter(slice.readmePath, { verified });
|
|
1182
|
+
emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, verified } }, json, [
|
|
1183
|
+
`Stamped ${slice.missionName}/${slice.name} verified: ${verified}`,
|
|
1184
|
+
]);
|
|
1185
|
+
}
|
|
1186
|
+
catch (err) {
|
|
1187
|
+
fail(err, json, out);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
function buildMissionVerifiedCommand() {
|
|
1192
|
+
return new Command("verified")
|
|
1193
|
+
.description("Stamp a mission's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
|
|
1194
|
+
.argument("<mission>", "Mission name")
|
|
1195
|
+
.option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
|
|
1196
|
+
.option("--json", "Machine-readable output")
|
|
1197
|
+
.action(async (missionName, opts, command) => {
|
|
1198
|
+
const out = makeStdout();
|
|
1199
|
+
const json = Boolean(opts.json);
|
|
1200
|
+
try {
|
|
1201
|
+
const source = validateAgainst(opts.against);
|
|
1202
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1203
|
+
const mission = findMission(missionsRoot, missionName);
|
|
1204
|
+
if (!mission.readmePath) {
|
|
1205
|
+
throw new ScopeCliError({
|
|
1206
|
+
fact: `Mission ${mission.name} has no README.md.`,
|
|
1207
|
+
consequence: "verified is a frontmatter field on the README; nothing to write.",
|
|
1208
|
+
action: "Create the mission README before stamping verified.",
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
const verified = `${todayDateISO()} against ${source}`;
|
|
1212
|
+
updateFrontmatter(mission.readmePath, { verified });
|
|
1213
|
+
emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, verified } }, json, [
|
|
1214
|
+
`Stamped ${mission.name} verified: ${verified}`,
|
|
1215
|
+
]);
|
|
1216
|
+
}
|
|
1217
|
+
catch (err) {
|
|
1218
|
+
fail(err, json, out);
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
/** Map a legacy `status:` to a §4 migration stage. Default `wip` when absent
|
|
1223
|
+
* or unmapped (the safe floor). */
|
|
1224
|
+
function mapLegacyStatusToStage(status) {
|
|
1225
|
+
const s = typeof status === "string" ? status.toLowerCase().trim() : "";
|
|
1226
|
+
if (s === "placeholder")
|
|
1227
|
+
return "wip";
|
|
1228
|
+
if (s === "draft" || s === "draft-for-comms")
|
|
1229
|
+
return "wip";
|
|
1230
|
+
if (s === "active" || s === "in-flight")
|
|
1231
|
+
return "established";
|
|
1232
|
+
if (s.startsWith("shipped") || s.startsWith("closed"))
|
|
1233
|
+
return "established";
|
|
1234
|
+
if (s === "ready-for-mission" || s === "ready-for-orch-dispatch")
|
|
1235
|
+
return "provisional";
|
|
1236
|
+
return "wip";
|
|
1237
|
+
}
|
|
1238
|
+
/** Idempotently conform a scope README's mandatory frontmatter. Only ADDS
|
|
1239
|
+
* absent fields — never clobbers an existing id/stage/verified. `mintId` is
|
|
1240
|
+
* called only when `id` is absent (it may persist a parent id per §1 lazy
|
|
1241
|
+
* adoption). */
|
|
1242
|
+
function conformReadmeFrontmatter(readmePath, mintId) {
|
|
1243
|
+
const fm = readFrontmatter(readmePath);
|
|
1244
|
+
const updates = {};
|
|
1245
|
+
let idAdded = null;
|
|
1246
|
+
const hasId = typeof fm.id === "string" && fm.id.length > 0;
|
|
1247
|
+
if (!hasId) {
|
|
1248
|
+
const minted = mintId();
|
|
1249
|
+
if (minted) {
|
|
1250
|
+
idAdded = minted;
|
|
1251
|
+
updates.id = minted;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
let stageAdded = null;
|
|
1255
|
+
if (!fm.stage) {
|
|
1256
|
+
stageAdded = mapLegacyStatusToStage(fm.status);
|
|
1257
|
+
updates.stage = stageAdded;
|
|
1258
|
+
}
|
|
1259
|
+
let verifiedAdded = null;
|
|
1260
|
+
if (!fm.verified) {
|
|
1261
|
+
verifiedAdded = `${todayDateISO()} against backfill (rig scope repair)`;
|
|
1262
|
+
updates.verified = verifiedAdded;
|
|
1263
|
+
}
|
|
1264
|
+
const changed = Object.keys(updates).length > 0;
|
|
1265
|
+
if (changed)
|
|
1266
|
+
updateFrontmatter(readmePath, updates);
|
|
1267
|
+
return { idAdded, stageAdded, verifiedAdded, changed };
|
|
1268
|
+
}
|
|
1269
|
+
/** Mint a slice's id from its (persisted) parent mission id + NN — the §1
|
|
1270
|
+
* lazy parent-ID adoption site. Null when the folder has no NN. */
|
|
1271
|
+
function mintSliceIdClosure(slice, missionsRoot) {
|
|
1272
|
+
return () => {
|
|
1273
|
+
if (slice.nn == null)
|
|
1274
|
+
return null;
|
|
1275
|
+
const mission = findMission(missionsRoot, slice.missionName);
|
|
1276
|
+
const missionId = ensureMissionIdPersisted(mission, missionsRoot);
|
|
1277
|
+
return sliceIdFromMission(missionId, slice.nn);
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function conformLines(scope, r) {
|
|
1281
|
+
if (!r.changed)
|
|
1282
|
+
return [` frontmatter: conformant (no change)`];
|
|
1283
|
+
const parts = [];
|
|
1284
|
+
if (r.idAdded)
|
|
1285
|
+
parts.push(`id=${r.idAdded}`);
|
|
1286
|
+
if (r.stageAdded)
|
|
1287
|
+
parts.push(`stage=${r.stageAdded}`);
|
|
1288
|
+
if (r.verifiedAdded)
|
|
1289
|
+
parts.push(`verified=${r.verifiedAdded}`);
|
|
1290
|
+
return [` frontmatter conformed: ${parts.join(", ")}`];
|
|
1291
|
+
}
|
|
1292
|
+
function buildSliceRepairCommand() {
|
|
1293
|
+
return new Command("repair")
|
|
1294
|
+
.description("Backfill a slice's missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified); idempotent")
|
|
1295
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
1296
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
1297
|
+
.option("--json", "Machine-readable output")
|
|
1298
|
+
.action(async (slicePath, opts, command) => {
|
|
1299
|
+
const out = makeStdout();
|
|
1300
|
+
const json = Boolean(opts.json);
|
|
1301
|
+
try {
|
|
1302
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1303
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
1304
|
+
const result = backfillScopeProgress(slice.absPath, "slice");
|
|
1305
|
+
const frontmatter = slice.readmePath
|
|
1306
|
+
? conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot))
|
|
1307
|
+
: { idAdded: null, stageAdded: null, verifiedAdded: null, changed: false };
|
|
1308
|
+
emit(out, { ok: true, result, frontmatter }, json, [
|
|
1309
|
+
`${result.created ? "Backfilled" : "Skipped"} ${slice.name}: ${result.reason}`,
|
|
1310
|
+
...(result.path ? [` path: ${result.path}`] : []),
|
|
1311
|
+
...conformLines("slice", frontmatter),
|
|
1312
|
+
]);
|
|
1313
|
+
}
|
|
1314
|
+
catch (err) {
|
|
1315
|
+
fail(err, json, out);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function buildMissionRepairCommand() {
|
|
1320
|
+
return new Command("repair")
|
|
1321
|
+
.description("Backfill missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified) for a mission and its slices; idempotent")
|
|
1322
|
+
.argument("<mission>", "Mission name")
|
|
1323
|
+
.option("--json", "Machine-readable output")
|
|
1324
|
+
.action(async (missionName, opts, command) => {
|
|
1325
|
+
const out = makeStdout();
|
|
1326
|
+
const json = Boolean(opts.json);
|
|
1327
|
+
try {
|
|
1328
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1329
|
+
const mission = findMission(missionsRoot, missionName);
|
|
1330
|
+
const results = [];
|
|
1331
|
+
results.push(backfillScopeProgress(mission.absPath, "mission"));
|
|
1332
|
+
const slicesDir = path.join(mission.absPath, "slices");
|
|
1333
|
+
if (fs.existsSync(slicesDir)) {
|
|
1334
|
+
for (const entry of fs.readdirSync(slicesDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1335
|
+
if (!entry.isDirectory() || !/^\d+-/.test(entry.name))
|
|
1336
|
+
continue;
|
|
1337
|
+
results.push(backfillScopeProgress(path.join(slicesDir, entry.name), "slice"));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// FR-4: conform mandatory frontmatter — mission first (mints+persists
|
|
1341
|
+
// the mission id), then each slice (child ids derive from the now-
|
|
1342
|
+
// persisted parent id).
|
|
1343
|
+
const conformed = [];
|
|
1344
|
+
if (mission.readmePath) {
|
|
1345
|
+
const fm = conformReadmeFrontmatter(mission.readmePath, () => ensureMissionId(mission, missionsRoot));
|
|
1346
|
+
conformed.push({ scope: "mission", name: mission.name, frontmatter: fm });
|
|
1347
|
+
}
|
|
1348
|
+
const freshMission = findMission(missionsRoot, mission.name);
|
|
1349
|
+
for (const slice of listSlices(freshMission, "all")) {
|
|
1350
|
+
if (!slice.readmePath)
|
|
1351
|
+
continue;
|
|
1352
|
+
const fm = conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot));
|
|
1353
|
+
conformed.push({ scope: "slice", name: slice.name, frontmatter: fm });
|
|
1354
|
+
}
|
|
1355
|
+
const created = results.filter((r) => r.created);
|
|
1356
|
+
const fmChanged = conformed.filter((c) => c.frontmatter.changed);
|
|
1357
|
+
emit(out, { ok: true, mission: mission.name, created, results, conformed }, json, [
|
|
1358
|
+
`Repaired ${mission.name}: ${created.length} PROGRESS.md backfilled, ${fmChanged.length} frontmatter conformed`,
|
|
1359
|
+
...created.map((r) => ` + PROGRESS ${r.scope}/${r.name}`),
|
|
1360
|
+
...fmChanged.map((c) => ` ~ frontmatter ${c.scope}/${c.name}`),
|
|
1361
|
+
]);
|
|
1362
|
+
}
|
|
1363
|
+
catch (err) {
|
|
1364
|
+
fail(err, json, out);
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
// ---------------------------------------------------------------------
|
|
636
1369
|
// Aggregate
|
|
637
1370
|
// ---------------------------------------------------------------------
|
|
638
1371
|
export function scopeCommand() {
|
|
@@ -646,12 +1379,21 @@ export function scopeCommand() {
|
|
|
646
1379
|
slice.addCommand(buildSliceShipCommand());
|
|
647
1380
|
slice.addCommand(buildSliceCloseCommand());
|
|
648
1381
|
slice.addCommand(buildSliceMoveCommand());
|
|
1382
|
+
slice.addCommand(buildSliceProgressCommand());
|
|
1383
|
+
slice.addCommand(buildSliceRepairCommand());
|
|
1384
|
+
slice.addCommand(buildSliceStageCommand());
|
|
1385
|
+
slice.addCommand(buildSliceVerifiedCommand());
|
|
649
1386
|
cmd.addCommand(slice);
|
|
650
1387
|
const mission = new Command("mission").description("Mission-tier commands");
|
|
651
1388
|
mission.addCommand(buildMissionLsCommand());
|
|
652
1389
|
mission.addCommand(buildMissionShowCommand());
|
|
653
1390
|
mission.addCommand(buildMissionCreateCommand());
|
|
1391
|
+
mission.addCommand(buildMissionProgressCommand());
|
|
1392
|
+
mission.addCommand(buildMissionRepairCommand());
|
|
1393
|
+
mission.addCommand(buildMissionStageCommand());
|
|
1394
|
+
mission.addCommand(buildMissionVerifiedCommand());
|
|
654
1395
|
cmd.addCommand(mission);
|
|
1396
|
+
cmd.addCommand(buildAuditCommand());
|
|
655
1397
|
return cmd;
|
|
656
1398
|
}
|
|
657
1399
|
// Re-exports for tests.
|