@openrig/cli 0.3.4-rc.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/daemon/assets/plugins/openrig-core/hooks/scripts/activity-relay.cjs +34 -0
- 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 +5 -1
- package/daemon/dist/adapters/codex-runtime-adapter.d.ts.map +1 -1
- package/daemon/dist/adapters/codex-runtime-adapter.js +58 -5
- 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/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 +11 -2
- package/daemon/dist/domain/agent-activity-store.js.map +1 -1
- 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-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 +4 -0
- package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
- package/daemon/dist/domain/queue-repository.js +21 -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 +26 -0
- package/daemon/dist/domain/scope/scope-audit.d.ts.map +1 -0
- package/daemon/dist/domain/scope/scope-audit.js +96 -0
- package/daemon/dist/domain/scope/scope-audit.js.map +1 -0
- package/daemon/dist/domain/scope/types.d.ts +61 -0
- package/daemon/dist/domain/scope/types.d.ts.map +1 -0
- package/daemon/dist/domain/scope/types.js +38 -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/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/startup-orchestrator.js +1 -1
- package/daemon/dist/domain/startup-orchestrator.js.map +1 -1
- package/daemon/dist/domain/types.d.ts +38 -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/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/queue.d.ts.map +1 -1
- package/daemon/dist/routes/queue.js +8 -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 +148 -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 +4 -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 +27 -8
- 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/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +6 -6
- package/dist/commands/capture.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 +60 -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 +688 -4
- 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 +6 -6
- 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 +2 -0
- package/dist/index.js.map +1 -1
- 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 +26 -0
- package/dist/lib/scope/scope-audit.d.ts.map +1 -0
- package/dist/lib/scope/scope-audit.js +96 -0
- package/dist/lib/scope/scope-audit.js.map +1 -0
- package/dist/lib/scope/templates.d.ts +2 -0
- package/dist/lib/scope/templates.d.ts.map +1 -1
- package/dist/lib/scope/templates.js +8 -0
- 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-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 +1 -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 +1 -1
- package/ui/dist/assets/index-DMI6jeAX.css +32 -0
- package/ui/dist/assets/index-FQgCB6mC.js +580 -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 { renderMissionNotesTemplate, renderMissionProgressTemplate, renderMissionTemplate, 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();
|
|
@@ -226,7 +243,16 @@ function buildSliceCreateCommand() {
|
|
|
226
243
|
});
|
|
227
244
|
fs.mkdirSync(sliceAbs, { recursive: true });
|
|
228
245
|
const readmePath = path.join(sliceAbs, "README.md");
|
|
229
|
-
|
|
246
|
+
const readmeOnly = Boolean(opts.readmeOnly);
|
|
247
|
+
if (readmeOnly) {
|
|
248
|
+
const markerBody = body.replace(/^(---\n)/, `---\nprogress_rail: readme-only\n`);
|
|
249
|
+
fs.writeFileSync(readmePath, markerBody, "utf8");
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
fs.writeFileSync(readmePath, body, "utf8");
|
|
253
|
+
const progressPath = path.join(sliceAbs, "PROGRESS.md");
|
|
254
|
+
fs.writeFileSync(progressPath, renderSliceProgressTemplate(title), "utf8");
|
|
255
|
+
}
|
|
230
256
|
const payload = {
|
|
231
257
|
ok: true,
|
|
232
258
|
slice: {
|
|
@@ -464,6 +490,8 @@ function buildMissionShowCommand() {
|
|
|
464
490
|
const slices = listSlices(mission, "all").map((s) => ({
|
|
465
491
|
name: s.name, id: s.id, status: s.status, nn: s.nn,
|
|
466
492
|
}));
|
|
493
|
+
// FR-5: derive read-time trust from (stage x verified) — NEVER stored.
|
|
494
|
+
const trust = deriveScopeTrust(mission.frontmatter);
|
|
467
495
|
const payload = {
|
|
468
496
|
ok: true,
|
|
469
497
|
mission: {
|
|
@@ -473,6 +501,7 @@ function buildMissionShowCommand() {
|
|
|
473
501
|
activeSliceCount: mission.activeSliceCount,
|
|
474
502
|
closedSliceCount: mission.closedSliceCount,
|
|
475
503
|
frontmatter: mission.frontmatter,
|
|
504
|
+
trust,
|
|
476
505
|
readme,
|
|
477
506
|
slices,
|
|
478
507
|
},
|
|
@@ -483,6 +512,7 @@ function buildMissionShowCommand() {
|
|
|
483
512
|
else {
|
|
484
513
|
out.write(`Mission: ${mission.name}\n`);
|
|
485
514
|
out.write(` id: ${mission.id ?? "—"}\n`);
|
|
515
|
+
out.write(formatTrustLine(trust));
|
|
486
516
|
out.write(` active slices: ${mission.activeSliceCount}\n`);
|
|
487
517
|
out.write(` closed slices: ${mission.closedSliceCount}\n`);
|
|
488
518
|
out.write(` path: ${mission.absPath}\n`);
|
|
@@ -593,11 +623,14 @@ function buildMissionCreateCommand() {
|
|
|
593
623
|
});
|
|
594
624
|
missionNotesRendered = { rendered: r.rendered, resolvedFrom: r.resolvedFrom };
|
|
595
625
|
}
|
|
626
|
+
const progressBody = renderMissionProgressTemplate(title);
|
|
596
627
|
// All renders succeeded — safe to touch the filesystem.
|
|
597
628
|
fs.mkdirSync(absPath, { recursive: true });
|
|
598
629
|
fs.mkdirSync(path.join(absPath, "slices"), { recursive: true });
|
|
599
630
|
const readmePath = path.join(absPath, "README.md");
|
|
600
631
|
fs.writeFileSync(readmePath, readmeBody, "utf8");
|
|
632
|
+
const progressPath = path.join(absPath, "PROGRESS.md");
|
|
633
|
+
fs.writeFileSync(progressPath, progressBody, "utf8");
|
|
601
634
|
let missionNotesPath = null;
|
|
602
635
|
let missionNotesResolvedFrom = null;
|
|
603
636
|
if (missionNotesRendered) {
|
|
@@ -633,6 +666,648 @@ function buildMissionCreateCommand() {
|
|
|
633
666
|
});
|
|
634
667
|
}
|
|
635
668
|
// ---------------------------------------------------------------------
|
|
669
|
+
// Audit (B2 — read-only scope audit)
|
|
670
|
+
// ---------------------------------------------------------------------
|
|
671
|
+
function buildAuditCommand() {
|
|
672
|
+
return new Command("audit")
|
|
673
|
+
.description("Read-only scope audit: flag missing/malformed progress rails and registration ghosts")
|
|
674
|
+
.requiredOption("--mission <name>", "Mission to audit")
|
|
675
|
+
.option("--json", "Machine-readable JSON output")
|
|
676
|
+
.action(async (opts, command) => {
|
|
677
|
+
const out = makeStdout();
|
|
678
|
+
const json = Boolean(opts.json);
|
|
679
|
+
try {
|
|
680
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
681
|
+
const { classifyScopeItem } = await import("../lib/scope/scope-audit.js");
|
|
682
|
+
const missionName = opts.mission;
|
|
683
|
+
const missionDir = path.join(missionsRoot, missionName);
|
|
684
|
+
if (!fs.existsSync(missionDir)) {
|
|
685
|
+
throw new ScopeCliError({ fact: `Mission "${missionName}" not found at ${missionDir}.`, consequence: "Cannot audit.", action: "Check the mission name." });
|
|
686
|
+
}
|
|
687
|
+
const missionReadme = path.join(missionDir, "README.md");
|
|
688
|
+
const missionProgress = path.join(missionDir, "PROGRESS.md");
|
|
689
|
+
const missionReadmeExists = fs.existsSync(missionReadme);
|
|
690
|
+
const missionProgressExists = fs.existsSync(missionProgress);
|
|
691
|
+
let missionResult;
|
|
692
|
+
if (!missionReadmeExists && missionProgressExists) {
|
|
693
|
+
missionResult = {
|
|
694
|
+
railStatus: "malformed",
|
|
695
|
+
findings: [{
|
|
696
|
+
kind: "orphan_progress",
|
|
697
|
+
severity: "high",
|
|
698
|
+
path: missionDir,
|
|
699
|
+
message: `PROGRESS.md exists but no README.md (orphan progress rail, no backing scope item)`,
|
|
700
|
+
remediation: `Add a README.md with frontmatter id, or remove the orphan PROGRESS.md`,
|
|
701
|
+
}],
|
|
702
|
+
frontmatterError: null,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
const missionFm = missionReadmeExists
|
|
707
|
+
? extractFrontmatterRaw(fs.readFileSync(missionReadme, "utf-8"))
|
|
708
|
+
: null;
|
|
709
|
+
missionResult = classifyScopeItem({
|
|
710
|
+
id: null,
|
|
711
|
+
path: missionDir,
|
|
712
|
+
readmeFrontmatterRaw: missionFm,
|
|
713
|
+
progressFileExists: missionProgressExists,
|
|
714
|
+
readmeOnlyMarker: false,
|
|
715
|
+
isActiveRelease: true,
|
|
716
|
+
level: "mission",
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const slicesDir = path.join(missionDir, "slices");
|
|
720
|
+
const sliceResults = [];
|
|
721
|
+
if (fs.existsSync(slicesDir)) {
|
|
722
|
+
for (const entry of fs.readdirSync(slicesDir)) {
|
|
723
|
+
const sliceDir = path.join(slicesDir, entry);
|
|
724
|
+
if (!fs.statSync(sliceDir).isDirectory())
|
|
725
|
+
continue;
|
|
726
|
+
const sliceReadme = path.join(sliceDir, "README.md");
|
|
727
|
+
const sliceProgress = path.join(sliceDir, "PROGRESS.md");
|
|
728
|
+
if (!fs.existsSync(sliceReadme)) {
|
|
729
|
+
if (fs.existsSync(sliceProgress)) {
|
|
730
|
+
sliceResults.push({
|
|
731
|
+
name: entry,
|
|
732
|
+
result: {
|
|
733
|
+
railStatus: "malformed",
|
|
734
|
+
findings: [{
|
|
735
|
+
kind: "orphan_progress",
|
|
736
|
+
severity: "high",
|
|
737
|
+
path: sliceDir,
|
|
738
|
+
message: `PROGRESS.md exists but no README.md (orphan progress rail, no backing scope item)`,
|
|
739
|
+
remediation: `Add a README.md with frontmatter id, or remove the orphan PROGRESS.md`,
|
|
740
|
+
}],
|
|
741
|
+
frontmatterError: null,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
const noReadmeResult = classifyScopeItem({
|
|
747
|
+
id: null,
|
|
748
|
+
path: sliceDir,
|
|
749
|
+
readmeFrontmatterRaw: null,
|
|
750
|
+
progressFileExists: false,
|
|
751
|
+
readmeOnlyMarker: false,
|
|
752
|
+
isActiveRelease: true,
|
|
753
|
+
level: "slice",
|
|
754
|
+
});
|
|
755
|
+
sliceResults.push({ name: entry, result: noReadmeResult });
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const sliceFm = extractFrontmatterRaw(fs.readFileSync(sliceReadme, "utf-8"));
|
|
760
|
+
const readmeOnlyMarker = sliceFm !== null && /^progress_rail\s*:\s*readme-only/m.test(sliceFm);
|
|
761
|
+
const sliceResult = classifyScopeItem({
|
|
762
|
+
id: null,
|
|
763
|
+
path: sliceDir,
|
|
764
|
+
readmeFrontmatterRaw: sliceFm,
|
|
765
|
+
progressFileExists: fs.existsSync(sliceProgress),
|
|
766
|
+
readmeOnlyMarker,
|
|
767
|
+
isActiveRelease: true,
|
|
768
|
+
level: "slice",
|
|
769
|
+
});
|
|
770
|
+
if (!/^\d{2}-/.test(entry)) {
|
|
771
|
+
sliceResult.findings.push({
|
|
772
|
+
kind: "id_convention_violation",
|
|
773
|
+
severity: "high",
|
|
774
|
+
path: sliceDir,
|
|
775
|
+
message: `Directory "${entry}" does not match the NN-slug slice naming convention (e.g. 01-my-slice)`,
|
|
776
|
+
remediation: `Rename to NN-slug format or move out of slices/`,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
sliceResults.push({ name: entry, result: sliceResult });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const allFindings = [
|
|
783
|
+
...missionResult.findings.map((f) => ({ ...f, scope: "mission", scopeName: missionName })),
|
|
784
|
+
...sliceResults.flatMap((s) => s.result.findings.map((f) => ({ ...f, scope: "slice", scopeName: s.name }))),
|
|
785
|
+
];
|
|
786
|
+
if (json) {
|
|
787
|
+
out.write(JSON.stringify({
|
|
788
|
+
ok: allFindings.length === 0,
|
|
789
|
+
mission: { name: missionName, railStatus: missionResult.railStatus, frontmatterError: missionResult.frontmatterError, findings: missionResult.findings },
|
|
790
|
+
slices: sliceResults.map((s) => ({ name: s.name, railStatus: s.result.railStatus, frontmatterError: s.result.frontmatterError, findings: s.result.findings })),
|
|
791
|
+
totalFindings: allFindings.length,
|
|
792
|
+
}, null, 2));
|
|
793
|
+
out.write("\n");
|
|
794
|
+
if (allFindings.length > 0)
|
|
795
|
+
process.exitCode = 1;
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
out.write(`Scope audit: ${missionName}\n`);
|
|
799
|
+
out.write(`Mission rail: ${missionResult.railStatus}\n`);
|
|
800
|
+
out.write(`Slices: ${sliceResults.length} total\n\n`);
|
|
801
|
+
if (allFindings.length > 0) {
|
|
802
|
+
out.write("FINDINGS:\n");
|
|
803
|
+
for (const f of allFindings) {
|
|
804
|
+
out.write(` [${f.severity}] [${f.kind}] ${f.scope}/${f.scopeName}\n`);
|
|
805
|
+
out.write(` ${f.message}\n`);
|
|
806
|
+
out.write(` fix: ${f.remediation}\n`);
|
|
807
|
+
}
|
|
808
|
+
out.write(`\nFAIL: ${allFindings.length} finding(s)\n`);
|
|
809
|
+
process.exitCode = 1;
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
out.write("PASS: all scope items have valid rails\n");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
if (err instanceof ScopeCliError) {
|
|
817
|
+
fail(err, json, out);
|
|
818
|
+
}
|
|
819
|
+
throw err;
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
function extractFrontmatterRaw(content) {
|
|
824
|
+
if (!content.startsWith("---"))
|
|
825
|
+
return null;
|
|
826
|
+
const match = /^---\s*\n([\s\S]*?)\n---/.exec(content);
|
|
827
|
+
return match ? match[1] : null;
|
|
828
|
+
}
|
|
829
|
+
// ---------------------------------------------------------------------
|
|
830
|
+
// rig scope <tier> progress (OPR.0.4.0.33 FR-3 — deterministic update)
|
|
831
|
+
// ---------------------------------------------------------------------
|
|
832
|
+
/** Resolve which file a progress update edits for a scope dir: the
|
|
833
|
+
* PROGRESS.md when present, else the README's rail for a readme-only
|
|
834
|
+
* scope, else an error directing to create/repair (the verb UPDATES an
|
|
835
|
+
* existing surface; it does not scaffold). */
|
|
836
|
+
function resolveProgressTarget(scopeDir, level) {
|
|
837
|
+
const progressPath = path.join(scopeDir, "PROGRESS.md");
|
|
838
|
+
if (fs.existsSync(progressPath))
|
|
839
|
+
return { targetPath: progressPath, kind: "progress" };
|
|
840
|
+
const readmePath = path.join(scopeDir, "README.md");
|
|
841
|
+
if (fs.existsSync(readmePath)) {
|
|
842
|
+
const fm = readFrontmatter(readmePath);
|
|
843
|
+
if (String(fm.progress_rail ?? "") === "readme-only") {
|
|
844
|
+
return { targetPath: readmePath, kind: "readme-only" };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
throw new ScopeCliError({
|
|
848
|
+
fact: `${level} at ${scopeDir} has no progress surface (no PROGRESS.md and no readme-only rail).`,
|
|
849
|
+
consequence: "The progress verb updates an existing surface; it does not scaffold.",
|
|
850
|
+
action: `Backfill it with: rig scope ${level} repair <target> (creates PROGRESS.md), or rig scope ${level} create.`,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/** Shared body for slice/mission progress: validate the mutually
|
|
854
|
+
* exclusive --add/--set modes, edit the resolved surface, write only
|
|
855
|
+
* on change. */
|
|
856
|
+
function runProgressUpdate(scopeDir, level, scopeName, opts, out, json) {
|
|
857
|
+
const hasAdd = typeof opts.add === "string";
|
|
858
|
+
const hasSet = typeof opts.set === "string";
|
|
859
|
+
if (hasAdd === hasSet) {
|
|
860
|
+
throw new ScopeCliError({
|
|
861
|
+
fact: hasAdd
|
|
862
|
+
? "Both --add and --set were given."
|
|
863
|
+
: "Neither --add nor --set was given.",
|
|
864
|
+
consequence: "No progress update was made.",
|
|
865
|
+
action: 'Pass exactly one of --add "<row text>" or --set "<row text>".',
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
const status = parseStatus(opts.status ?? "active");
|
|
869
|
+
const { targetPath, kind } = resolveProgressTarget(scopeDir, level);
|
|
870
|
+
const before = fs.readFileSync(targetPath, "utf8");
|
|
871
|
+
let result;
|
|
872
|
+
let operation;
|
|
873
|
+
if (hasAdd) {
|
|
874
|
+
operation = "add";
|
|
875
|
+
result = addProgressRow(before, {
|
|
876
|
+
section: opts.section ?? DEFAULT_PROGRESS_SECTION,
|
|
877
|
+
text: opts.add,
|
|
878
|
+
status,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
operation = "set";
|
|
883
|
+
result = setProgressRow(before, { text: opts.set, status });
|
|
884
|
+
}
|
|
885
|
+
if (result.changed)
|
|
886
|
+
fs.writeFileSync(targetPath, result.content, "utf8");
|
|
887
|
+
emit(out, {
|
|
888
|
+
ok: true,
|
|
889
|
+
progress: {
|
|
890
|
+
scope: level,
|
|
891
|
+
name: scopeName,
|
|
892
|
+
target: targetPath,
|
|
893
|
+
kind,
|
|
894
|
+
operation,
|
|
895
|
+
status,
|
|
896
|
+
changed: result.changed,
|
|
897
|
+
},
|
|
898
|
+
}, json, [
|
|
899
|
+
`${result.changed ? "Updated" : "No change"} ${level} ${scopeName} progress (${operation})`,
|
|
900
|
+
` target: ${targetPath}`,
|
|
901
|
+
` status: ${status}`,
|
|
902
|
+
]);
|
|
903
|
+
}
|
|
904
|
+
function buildSliceProgressCommand() {
|
|
905
|
+
return new Command("progress")
|
|
906
|
+
.description("Update a slice's progress rail deterministically (append a row, or set a row's status)")
|
|
907
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
908
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
909
|
+
.option("--add <text>", "Append a checkbox row with this text")
|
|
910
|
+
.option("--set <text>", "Set the status of the row whose trimmed text exactly matches")
|
|
911
|
+
.option("--section <heading>", `Section heading for --add (default: ${DEFAULT_PROGRESS_SECTION})`)
|
|
912
|
+
.option("--status <status>", `Row status: ${PROGRESS_STATUSES.join(" | ")}`, "active")
|
|
913
|
+
.option("--json", "Machine-readable output")
|
|
914
|
+
.action(async (slicePath, opts, command) => {
|
|
915
|
+
const out = makeStdout();
|
|
916
|
+
const json = Boolean(opts.json);
|
|
917
|
+
try {
|
|
918
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
919
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
920
|
+
runProgressUpdate(slice.absPath, "slice", slice.name, opts, out, json);
|
|
921
|
+
}
|
|
922
|
+
catch (err) {
|
|
923
|
+
fail(err, json, out);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function buildMissionProgressCommand() {
|
|
928
|
+
return new Command("progress")
|
|
929
|
+
.description("Update a mission's progress rail deterministically (append a row, or set a row's status)")
|
|
930
|
+
.argument("<mission>", "Mission name")
|
|
931
|
+
.option("--add <text>", "Append a checkbox row with this text")
|
|
932
|
+
.option("--set <text>", "Set the status of the row whose trimmed text exactly matches")
|
|
933
|
+
.option("--section <heading>", `Section heading for --add (default: ${DEFAULT_PROGRESS_SECTION})`)
|
|
934
|
+
.option("--status <status>", `Row status: ${PROGRESS_STATUSES.join(" | ")}`, "active")
|
|
935
|
+
.option("--json", "Machine-readable output")
|
|
936
|
+
.action(async (missionName, opts, command) => {
|
|
937
|
+
const out = makeStdout();
|
|
938
|
+
const json = Boolean(opts.json);
|
|
939
|
+
try {
|
|
940
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
941
|
+
const mission = findMission(missionsRoot, missionName);
|
|
942
|
+
runProgressUpdate(mission.absPath, "mission", mission.name, opts, out, json);
|
|
943
|
+
}
|
|
944
|
+
catch (err) {
|
|
945
|
+
fail(err, json, out);
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
/** Mirror the create-time title derivation so a backfilled PROGRESS.md
|
|
950
|
+
* is byte-identical to what create would have written. */
|
|
951
|
+
function backfillTitle(level, scopeDir) {
|
|
952
|
+
const base = path.basename(scopeDir);
|
|
953
|
+
return level === "mission"
|
|
954
|
+
? titleFromSlug(base.replace(/^release-/, ""))
|
|
955
|
+
: titleFromSlug(base.replace(/^\d+-/, ""));
|
|
956
|
+
}
|
|
957
|
+
/** Create a missing PROGRESS.md for a single scope dir. Idempotent
|
|
958
|
+
* (skips when one exists) and non-clobbering (skips an intentional
|
|
959
|
+
* readme-only scope, and skips README-less dirs that are not declared
|
|
960
|
+
* scopes). */
|
|
961
|
+
function backfillScopeProgress(scopeDir, level) {
|
|
962
|
+
const name = path.basename(scopeDir);
|
|
963
|
+
const readmePath = path.join(scopeDir, "README.md");
|
|
964
|
+
if (!fs.existsSync(readmePath)) {
|
|
965
|
+
return { scope: level, name, created: false, reason: "no-readme (not a declared scope)", path: null };
|
|
966
|
+
}
|
|
967
|
+
const progressPath = path.join(scopeDir, "PROGRESS.md");
|
|
968
|
+
if (fs.existsSync(progressPath)) {
|
|
969
|
+
return { scope: level, name, created: false, reason: "already-present", path: progressPath };
|
|
970
|
+
}
|
|
971
|
+
const fm = readFrontmatter(readmePath);
|
|
972
|
+
if (String(fm.progress_rail ?? "") === "readme-only") {
|
|
973
|
+
return { scope: level, name, created: false, reason: "readme-only (intentional opt-out)", path: null };
|
|
974
|
+
}
|
|
975
|
+
const title = backfillTitle(level, scopeDir);
|
|
976
|
+
const body = level === "mission"
|
|
977
|
+
? renderMissionProgressTemplate(title)
|
|
978
|
+
: renderSliceProgressTemplate(title);
|
|
979
|
+
fs.writeFileSync(progressPath, body, "utf8");
|
|
980
|
+
return { scope: level, name, created: true, reason: "backfilled", path: progressPath };
|
|
981
|
+
}
|
|
982
|
+
// ---------------------------------------------------------------------
|
|
983
|
+
// OPR.0.4.1.6 — stage + verified verbs (deterministic §2 maturity edits)
|
|
984
|
+
// ---------------------------------------------------------------------
|
|
985
|
+
/** Validate a stage against the §2 enum, rejecting invented values. */
|
|
986
|
+
function validateStage(raw) {
|
|
987
|
+
if (!STAGE_VALUES.includes(raw)) {
|
|
988
|
+
throw new ScopeCliError({
|
|
989
|
+
fact: `Invalid stage "${raw}".`,
|
|
990
|
+
consequence: "Stage not changed.",
|
|
991
|
+
action: `Use one of: ${STAGE_VALUES.join(" | ")}.`,
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
return raw;
|
|
995
|
+
}
|
|
996
|
+
/** Surgically set `stage` (+ `superseded-by` when superseded) on a scope
|
|
997
|
+
* README, enforcing the §2 superseded-needs-successor rule. */
|
|
998
|
+
function applyStage(readmePath, stage, successor) {
|
|
999
|
+
const updates = { stage };
|
|
1000
|
+
if (stage === "superseded") {
|
|
1001
|
+
const id = typeof successor === "string" ? successor.trim() : "";
|
|
1002
|
+
if (!id) {
|
|
1003
|
+
throw new ScopeCliError({
|
|
1004
|
+
fact: "stage 'superseded' requires a successor.",
|
|
1005
|
+
consequence: "Stage not changed (a superseded scope must name what replaces it, per scope-and-versioning §2).",
|
|
1006
|
+
action: "Re-run with --successor <id>, e.g. --successor OPR.0.4.1.7.",
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
updates["superseded-by"] = id;
|
|
1010
|
+
}
|
|
1011
|
+
updateFrontmatter(readmePath, updates);
|
|
1012
|
+
}
|
|
1013
|
+
/** `retired` is an exit, not a rung — warn (do not block). */
|
|
1014
|
+
function warnRetired(stage) {
|
|
1015
|
+
if (stage === "retired") {
|
|
1016
|
+
process.stderr.write("Warning: stage 'retired' means do-not-use (an exit, not a maturity rung).\n");
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/** Validate a --against provenance: mandatory, non-empty, non-whitespace
|
|
1020
|
+
* (the §2 "no bare timestamp" rule). Returns the trimmed source. */
|
|
1021
|
+
function validateAgainst(raw) {
|
|
1022
|
+
const source = typeof raw === "string" ? raw.trim() : "";
|
|
1023
|
+
if (!source) {
|
|
1024
|
+
throw new ScopeCliError({
|
|
1025
|
+
fact: "--against provenance is empty or missing.",
|
|
1026
|
+
consequence: "verified not stamped — scope-and-versioning §2 forbids a bare timestamp without a named source.",
|
|
1027
|
+
action: 'Provide what it was verified against, e.g. --against "runtime (npm+tag+origin)".',
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return source;
|
|
1031
|
+
}
|
|
1032
|
+
function buildSliceStageCommand() {
|
|
1033
|
+
return new Command("stage")
|
|
1034
|
+
.description(`Set a slice's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
|
|
1035
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
1036
|
+
.argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
|
|
1037
|
+
.option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
|
|
1038
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
1039
|
+
.option("--json", "Machine-readable output")
|
|
1040
|
+
.action(async (slicePath, newStage, opts, command) => {
|
|
1041
|
+
const out = makeStdout();
|
|
1042
|
+
const json = Boolean(opts.json);
|
|
1043
|
+
try {
|
|
1044
|
+
const stage = validateStage(newStage);
|
|
1045
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1046
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
1047
|
+
if (!slice.readmePath) {
|
|
1048
|
+
throw new ScopeCliError({
|
|
1049
|
+
fact: `Slice ${slice.name} has no README.md.`,
|
|
1050
|
+
consequence: "Stage is a frontmatter field on the README; nothing to write.",
|
|
1051
|
+
action: "Create the slice README (rig scope slice create) before setting its stage.",
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
applyStage(slice.readmePath, stage, opts.successor);
|
|
1055
|
+
warnRetired(stage);
|
|
1056
|
+
const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
|
|
1057
|
+
emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
|
|
1058
|
+
`Set ${slice.missionName}/${slice.name} stage: ${stage}`,
|
|
1059
|
+
...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
|
|
1060
|
+
]);
|
|
1061
|
+
}
|
|
1062
|
+
catch (err) {
|
|
1063
|
+
fail(err, json, out);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
function buildMissionStageCommand() {
|
|
1068
|
+
return new Command("stage")
|
|
1069
|
+
.description(`Set a mission's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
|
|
1070
|
+
.argument("<mission>", "Mission name")
|
|
1071
|
+
.argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
|
|
1072
|
+
.option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
|
|
1073
|
+
.option("--json", "Machine-readable output")
|
|
1074
|
+
.action(async (missionName, newStage, opts, command) => {
|
|
1075
|
+
const out = makeStdout();
|
|
1076
|
+
const json = Boolean(opts.json);
|
|
1077
|
+
try {
|
|
1078
|
+
const stage = validateStage(newStage);
|
|
1079
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1080
|
+
const mission = findMission(missionsRoot, missionName);
|
|
1081
|
+
if (!mission.readmePath) {
|
|
1082
|
+
throw new ScopeCliError({
|
|
1083
|
+
fact: `Mission ${mission.name} has no README.md.`,
|
|
1084
|
+
consequence: "Stage is a frontmatter field on the README; nothing to write.",
|
|
1085
|
+
action: "Create the mission README before setting its stage.",
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
applyStage(mission.readmePath, stage, opts.successor);
|
|
1089
|
+
warnRetired(stage);
|
|
1090
|
+
const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
|
|
1091
|
+
emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
|
|
1092
|
+
`Set ${mission.name} stage: ${stage}`,
|
|
1093
|
+
...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
|
|
1094
|
+
]);
|
|
1095
|
+
}
|
|
1096
|
+
catch (err) {
|
|
1097
|
+
fail(err, json, out);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
function buildSliceVerifiedCommand() {
|
|
1102
|
+
return new Command("verified")
|
|
1103
|
+
.description("Stamp a slice's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
|
|
1104
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
1105
|
+
.option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
|
|
1106
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
1107
|
+
.option("--json", "Machine-readable output")
|
|
1108
|
+
.action(async (slicePath, opts, command) => {
|
|
1109
|
+
const out = makeStdout();
|
|
1110
|
+
const json = Boolean(opts.json);
|
|
1111
|
+
try {
|
|
1112
|
+
const source = validateAgainst(opts.against);
|
|
1113
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1114
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
1115
|
+
if (!slice.readmePath) {
|
|
1116
|
+
throw new ScopeCliError({
|
|
1117
|
+
fact: `Slice ${slice.name} has no README.md.`,
|
|
1118
|
+
consequence: "verified is a frontmatter field on the README; nothing to write.",
|
|
1119
|
+
action: "Create the slice README before stamping verified.",
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
const verified = `${todayDateISO()} against ${source}`;
|
|
1123
|
+
updateFrontmatter(slice.readmePath, { verified });
|
|
1124
|
+
emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, verified } }, json, [
|
|
1125
|
+
`Stamped ${slice.missionName}/${slice.name} verified: ${verified}`,
|
|
1126
|
+
]);
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
fail(err, json, out);
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
function buildMissionVerifiedCommand() {
|
|
1134
|
+
return new Command("verified")
|
|
1135
|
+
.description("Stamp a mission's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
|
|
1136
|
+
.argument("<mission>", "Mission name")
|
|
1137
|
+
.option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
|
|
1138
|
+
.option("--json", "Machine-readable output")
|
|
1139
|
+
.action(async (missionName, opts, command) => {
|
|
1140
|
+
const out = makeStdout();
|
|
1141
|
+
const json = Boolean(opts.json);
|
|
1142
|
+
try {
|
|
1143
|
+
const source = validateAgainst(opts.against);
|
|
1144
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1145
|
+
const mission = findMission(missionsRoot, missionName);
|
|
1146
|
+
if (!mission.readmePath) {
|
|
1147
|
+
throw new ScopeCliError({
|
|
1148
|
+
fact: `Mission ${mission.name} has no README.md.`,
|
|
1149
|
+
consequence: "verified is a frontmatter field on the README; nothing to write.",
|
|
1150
|
+
action: "Create the mission README before stamping verified.",
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
const verified = `${todayDateISO()} against ${source}`;
|
|
1154
|
+
updateFrontmatter(mission.readmePath, { verified });
|
|
1155
|
+
emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, verified } }, json, [
|
|
1156
|
+
`Stamped ${mission.name} verified: ${verified}`,
|
|
1157
|
+
]);
|
|
1158
|
+
}
|
|
1159
|
+
catch (err) {
|
|
1160
|
+
fail(err, json, out);
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
/** Map a legacy `status:` to a §4 migration stage. Default `wip` when absent
|
|
1165
|
+
* or unmapped (the safe floor). */
|
|
1166
|
+
function mapLegacyStatusToStage(status) {
|
|
1167
|
+
const s = typeof status === "string" ? status.toLowerCase().trim() : "";
|
|
1168
|
+
if (s === "placeholder")
|
|
1169
|
+
return "wip";
|
|
1170
|
+
if (s === "draft" || s === "draft-for-comms")
|
|
1171
|
+
return "wip";
|
|
1172
|
+
if (s === "active" || s === "in-flight")
|
|
1173
|
+
return "established";
|
|
1174
|
+
if (s.startsWith("shipped") || s.startsWith("closed"))
|
|
1175
|
+
return "established";
|
|
1176
|
+
if (s === "ready-for-mission" || s === "ready-for-orch-dispatch")
|
|
1177
|
+
return "provisional";
|
|
1178
|
+
return "wip";
|
|
1179
|
+
}
|
|
1180
|
+
/** Idempotently conform a scope README's mandatory frontmatter. Only ADDS
|
|
1181
|
+
* absent fields — never clobbers an existing id/stage/verified. `mintId` is
|
|
1182
|
+
* called only when `id` is absent (it may persist a parent id per §1 lazy
|
|
1183
|
+
* adoption). */
|
|
1184
|
+
function conformReadmeFrontmatter(readmePath, mintId) {
|
|
1185
|
+
const fm = readFrontmatter(readmePath);
|
|
1186
|
+
const updates = {};
|
|
1187
|
+
let idAdded = null;
|
|
1188
|
+
const hasId = typeof fm.id === "string" && fm.id.length > 0;
|
|
1189
|
+
if (!hasId) {
|
|
1190
|
+
const minted = mintId();
|
|
1191
|
+
if (minted) {
|
|
1192
|
+
idAdded = minted;
|
|
1193
|
+
updates.id = minted;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
let stageAdded = null;
|
|
1197
|
+
if (!fm.stage) {
|
|
1198
|
+
stageAdded = mapLegacyStatusToStage(fm.status);
|
|
1199
|
+
updates.stage = stageAdded;
|
|
1200
|
+
}
|
|
1201
|
+
let verifiedAdded = null;
|
|
1202
|
+
if (!fm.verified) {
|
|
1203
|
+
verifiedAdded = `${todayDateISO()} against backfill (rig scope repair)`;
|
|
1204
|
+
updates.verified = verifiedAdded;
|
|
1205
|
+
}
|
|
1206
|
+
const changed = Object.keys(updates).length > 0;
|
|
1207
|
+
if (changed)
|
|
1208
|
+
updateFrontmatter(readmePath, updates);
|
|
1209
|
+
return { idAdded, stageAdded, verifiedAdded, changed };
|
|
1210
|
+
}
|
|
1211
|
+
/** Mint a slice's id from its (persisted) parent mission id + NN — the §1
|
|
1212
|
+
* lazy parent-ID adoption site. Null when the folder has no NN. */
|
|
1213
|
+
function mintSliceIdClosure(slice, missionsRoot) {
|
|
1214
|
+
return () => {
|
|
1215
|
+
if (slice.nn == null)
|
|
1216
|
+
return null;
|
|
1217
|
+
const mission = findMission(missionsRoot, slice.missionName);
|
|
1218
|
+
const missionId = ensureMissionIdPersisted(mission, missionsRoot);
|
|
1219
|
+
return sliceIdFromMission(missionId, slice.nn);
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
function conformLines(scope, r) {
|
|
1223
|
+
if (!r.changed)
|
|
1224
|
+
return [` frontmatter: conformant (no change)`];
|
|
1225
|
+
const parts = [];
|
|
1226
|
+
if (r.idAdded)
|
|
1227
|
+
parts.push(`id=${r.idAdded}`);
|
|
1228
|
+
if (r.stageAdded)
|
|
1229
|
+
parts.push(`stage=${r.stageAdded}`);
|
|
1230
|
+
if (r.verifiedAdded)
|
|
1231
|
+
parts.push(`verified=${r.verifiedAdded}`);
|
|
1232
|
+
return [` frontmatter conformed: ${parts.join(", ")}`];
|
|
1233
|
+
}
|
|
1234
|
+
function buildSliceRepairCommand() {
|
|
1235
|
+
return new Command("repair")
|
|
1236
|
+
.description("Backfill a slice's missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified); idempotent")
|
|
1237
|
+
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
|
|
1238
|
+
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
|
|
1239
|
+
.option("--json", "Machine-readable output")
|
|
1240
|
+
.action(async (slicePath, opts, command) => {
|
|
1241
|
+
const out = makeStdout();
|
|
1242
|
+
const json = Boolean(opts.json);
|
|
1243
|
+
try {
|
|
1244
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1245
|
+
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
|
|
1246
|
+
const result = backfillScopeProgress(slice.absPath, "slice");
|
|
1247
|
+
const frontmatter = slice.readmePath
|
|
1248
|
+
? conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot))
|
|
1249
|
+
: { idAdded: null, stageAdded: null, verifiedAdded: null, changed: false };
|
|
1250
|
+
emit(out, { ok: true, result, frontmatter }, json, [
|
|
1251
|
+
`${result.created ? "Backfilled" : "Skipped"} ${slice.name}: ${result.reason}`,
|
|
1252
|
+
...(result.path ? [` path: ${result.path}`] : []),
|
|
1253
|
+
...conformLines("slice", frontmatter),
|
|
1254
|
+
]);
|
|
1255
|
+
}
|
|
1256
|
+
catch (err) {
|
|
1257
|
+
fail(err, json, out);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
function buildMissionRepairCommand() {
|
|
1262
|
+
return new Command("repair")
|
|
1263
|
+
.description("Backfill missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified) for a mission and its slices; idempotent")
|
|
1264
|
+
.argument("<mission>", "Mission name")
|
|
1265
|
+
.option("--json", "Machine-readable output")
|
|
1266
|
+
.action(async (missionName, opts, command) => {
|
|
1267
|
+
const out = makeStdout();
|
|
1268
|
+
const json = Boolean(opts.json);
|
|
1269
|
+
try {
|
|
1270
|
+
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
|
|
1271
|
+
const mission = findMission(missionsRoot, missionName);
|
|
1272
|
+
const results = [];
|
|
1273
|
+
results.push(backfillScopeProgress(mission.absPath, "mission"));
|
|
1274
|
+
const slicesDir = path.join(mission.absPath, "slices");
|
|
1275
|
+
if (fs.existsSync(slicesDir)) {
|
|
1276
|
+
for (const entry of fs.readdirSync(slicesDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1277
|
+
if (!entry.isDirectory() || !/^\d+-/.test(entry.name))
|
|
1278
|
+
continue;
|
|
1279
|
+
results.push(backfillScopeProgress(path.join(slicesDir, entry.name), "slice"));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
// FR-4: conform mandatory frontmatter — mission first (mints+persists
|
|
1283
|
+
// the mission id), then each slice (child ids derive from the now-
|
|
1284
|
+
// persisted parent id).
|
|
1285
|
+
const conformed = [];
|
|
1286
|
+
if (mission.readmePath) {
|
|
1287
|
+
const fm = conformReadmeFrontmatter(mission.readmePath, () => ensureMissionId(mission, missionsRoot));
|
|
1288
|
+
conformed.push({ scope: "mission", name: mission.name, frontmatter: fm });
|
|
1289
|
+
}
|
|
1290
|
+
const freshMission = findMission(missionsRoot, mission.name);
|
|
1291
|
+
for (const slice of listSlices(freshMission, "all")) {
|
|
1292
|
+
if (!slice.readmePath)
|
|
1293
|
+
continue;
|
|
1294
|
+
const fm = conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot));
|
|
1295
|
+
conformed.push({ scope: "slice", name: slice.name, frontmatter: fm });
|
|
1296
|
+
}
|
|
1297
|
+
const created = results.filter((r) => r.created);
|
|
1298
|
+
const fmChanged = conformed.filter((c) => c.frontmatter.changed);
|
|
1299
|
+
emit(out, { ok: true, mission: mission.name, created, results, conformed }, json, [
|
|
1300
|
+
`Repaired ${mission.name}: ${created.length} PROGRESS.md backfilled, ${fmChanged.length} frontmatter conformed`,
|
|
1301
|
+
...created.map((r) => ` + PROGRESS ${r.scope}/${r.name}`),
|
|
1302
|
+
...fmChanged.map((c) => ` ~ frontmatter ${c.scope}/${c.name}`),
|
|
1303
|
+
]);
|
|
1304
|
+
}
|
|
1305
|
+
catch (err) {
|
|
1306
|
+
fail(err, json, out);
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
// ---------------------------------------------------------------------
|
|
636
1311
|
// Aggregate
|
|
637
1312
|
// ---------------------------------------------------------------------
|
|
638
1313
|
export function scopeCommand() {
|
|
@@ -646,12 +1321,21 @@ export function scopeCommand() {
|
|
|
646
1321
|
slice.addCommand(buildSliceShipCommand());
|
|
647
1322
|
slice.addCommand(buildSliceCloseCommand());
|
|
648
1323
|
slice.addCommand(buildSliceMoveCommand());
|
|
1324
|
+
slice.addCommand(buildSliceProgressCommand());
|
|
1325
|
+
slice.addCommand(buildSliceRepairCommand());
|
|
1326
|
+
slice.addCommand(buildSliceStageCommand());
|
|
1327
|
+
slice.addCommand(buildSliceVerifiedCommand());
|
|
649
1328
|
cmd.addCommand(slice);
|
|
650
1329
|
const mission = new Command("mission").description("Mission-tier commands");
|
|
651
1330
|
mission.addCommand(buildMissionLsCommand());
|
|
652
1331
|
mission.addCommand(buildMissionShowCommand());
|
|
653
1332
|
mission.addCommand(buildMissionCreateCommand());
|
|
1333
|
+
mission.addCommand(buildMissionProgressCommand());
|
|
1334
|
+
mission.addCommand(buildMissionRepairCommand());
|
|
1335
|
+
mission.addCommand(buildMissionStageCommand());
|
|
1336
|
+
mission.addCommand(buildMissionVerifiedCommand());
|
|
654
1337
|
cmd.addCommand(mission);
|
|
1338
|
+
cmd.addCommand(buildAuditCommand());
|
|
655
1339
|
return cmd;
|
|
656
1340
|
}
|
|
657
1341
|
// Re-exports for tests.
|