@rig-c/create 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/docs/AGENT-REFERENCE.md +62 -0
- package/docs/CHECKS.md +45 -9
- package/docs/INTERVIEW.md +19 -7
- package/package.json +2 -1
- package/src/agent-guide.ts +7 -4
- package/src/check.ts +7 -4
- package/src/choice-writers.ts +3 -14
- package/src/cli.ts +1 -0
- package/src/feedback.ts +3 -10
- package/src/fix-ledger.ts +1 -1
- package/src/job-acts.ts +4 -0
- package/src/job-host-state.ts +154 -0
- package/src/job-host.ts +37 -14
- package/src/job-outbox.ts +5 -3
- package/src/job-publication.ts +17 -13
- package/src/job-runtime.ts +17 -4
- package/src/next.ts +21 -13
- package/src/parts-review.ts +4 -11
- package/src/reopen.ts +72 -9
- package/src/review-act.ts +49 -14
- package/src/review-package.ts +3 -1
- package/src/rig-version.ts +219 -8
- package/src/scene-writers.ts +5 -3
- package/src/scene.ts +14 -4
- package/src/signable.ts +25 -3
- package/src/signoff-checkpoint-act.ts +2 -16
- package/src/signoff.ts +3 -15
- package/src/waypoints.ts +20 -9
- package/templates/agent-guide.md +2 -24
- package/templates/collection.json +10 -2
- package/templates/example-chain/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/AGENTS.md +4 -26
- package/templates/example-chain/README.md +1 -1
- package/templates/example-chain/chain.json +385 -319
- package/templates/example-chain/checkpoints/0a-interview-options/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/0a-interview-options/project.json +22 -5
- package/templates/example-chain/checkpoints/0a-interview-options.json +7 -3
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/0b-plan-pending/project.json +22 -5
- package/templates/example-chain/checkpoints/0b-plan-pending.json +7 -3
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/1-parts-verdict/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/1-parts-verdict/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/1-parts-verdict/project.json +24 -7
- package/templates/example-chain/checkpoints/1-parts-verdict.json +9 -5
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/2-rigging-answers/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/2-rigging-answers/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/2-rigging-answers/project.json +24 -7
- package/templates/example-chain/checkpoints/2-rigging-answers.json +21 -5
- package/templates/example-chain/checkpoints/3-rig-review/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/3-rig-review/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/motion.json +1 -1
- package/templates/example-chain/checkpoints/3-rig-review/evidence/interview/turn.json +108 -0
- package/templates/example-chain/checkpoints/3-rig-review/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/3-rig-review/project.json +77 -28
- package/templates/example-chain/checkpoints/3-rig-review.json +19 -11
- package/templates/example-chain/checkpoints/4-direction-post/AGENT-REFERENCE.md +62 -0
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +4 -26
- package/templates/example-chain/checkpoints/4-direction-post/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/camera_depth.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/motion.json +1 -1
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_composition.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/post_timing.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/evidence/interview/turn.json +108 -0
- package/templates/example-chain/checkpoints/4-direction-post/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-chain/checkpoints/4-direction-post/project.json +106 -57
- package/templates/example-chain/checkpoints/4-direction-post.json +22 -14
- package/templates/example-chain/derived/drafts/chain/out/check/build/run.json +2 -2
- package/templates/example-chain/derived/drafts/chain/out/run.json +2 -2
- package/templates/example-chain/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-chain/evidence/captions.json +1 -1
- package/templates/example-chain/evidence/direction/v2.json +1 -1
- package/templates/example-chain/evidence/direction-input.json +1 -1
- package/templates/example-chain/evidence/interview/camera_depth.json +2 -2
- package/templates/example-chain/evidence/interview/motion.json +1 -1
- package/templates/example-chain/evidence/interview/post_composition.json +2 -2
- package/templates/example-chain/evidence/interview/post_timing.json +2 -2
- package/templates/example-chain/evidence/interview/turn.json +108 -0
- package/templates/example-chain/evidence/scene/s1.json +2 -2
- package/templates/example-chain/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-chain/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-chain/project.json +127 -78
- package/templates/example-direction/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/drafts/lantern/out/run.json +2 -2
- package/templates/example-draft/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-draft/derived/review/v2/out/check/build/run.json +2 -2
- package/templates/example-draft/derived/review/v2/out/rig/run.json +2 -2
- package/templates/example-draft/evidence/direction/v4.json +3 -3
- package/templates/example-draft/evidence/direction-input-v4.json +3 -3
- package/templates/example-draft/evidence/parts-review.json +1 -1
- package/templates/example-draft/evidence/review-reads/{52673b635b9cbc92da1a9e3d4c023a249bbacf79cd21b59724f03f18f8caf09b.json → 8866887a00012b5d5944b7525a478aea70931935bff35ddfcb186ffc515ef5af.json} +2 -2
- package/templates/example-draft/evidence/scene/s1.json +3 -3
- package/templates/example-draft/evidence/scene-citations.json +1 -1
- package/templates/example-draft/out/check/build/run.json +2 -2
- package/templates/example-draft/out/run.json +2 -2
- package/templates/example-draft/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-draft/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-draft/outputs/v2/out/check/build/run.json +2 -2
- package/templates/example-draft/outputs/v2/out/rig/run.json +2 -2
- package/templates/example-interview-choice.json +113 -68
- package/templates/example-interview-options/constraint.json +2 -2
- package/templates/example-interview-options/example-assemble.json +2 -2
- package/templates/example-interview-options/example-bones.json +5 -5
- package/templates/example-interview-options/example-camera-depth.json +3 -3
- package/templates/example-interview-options/example-constraints.json +5 -5
- package/templates/example-interview-options/example-meshes.json +5 -5
- package/templates/example-interview-options/example-motion.json +5 -5
- package/templates/example-interview-options/example-picks.json +2 -2
- package/templates/example-interview-options/example-post-composition.json +3 -3
- package/templates/example-interview-options/example-post-timing.json +3 -3
- package/templates/example-interview-options/example-reference.json +2 -2
- package/templates/example-interview-options/example-rig-type.json +2 -2
- package/templates/example-interview-options/example-seethrough.json +2 -2
- package/templates/example-interview-options/example-turn.json +108 -0
- package/templates/example-interview-options/fullscreen.json +2 -2
- package/templates/example-interview-options/other-explicit.json +2 -2
- package/templates/example-interview-options/other-pending.json +2 -2
- package/templates/example-interview-options/other-picks.json +2 -2
- package/templates/example-invariants/invariants.json +223 -164
- package/templates/example-project.json +301 -249
- package/templates/example-remesh/AGENT-REFERENCE.md +62 -0
- package/templates/example-remesh/AGENTS.md +4 -26
- package/templates/example-remesh/derived/drafts/remesh/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/drafts/remesh/out/run.json +2 -2
- package/templates/example-remesh/derived/review/v1/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/review/v1/out/rig/run.json +2 -2
- package/templates/example-remesh/derived/review/v2/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/review/v2/out/rig/run.json +2 -2
- package/templates/example-remesh/derived/review/v3/out/check/build/run.json +2 -2
- package/templates/example-remesh/derived/review/v3/out/rig/run.json +2 -2
- package/templates/example-remesh/evidence/captions.json +1 -1
- package/templates/example-remesh/evidence/direction/v4.json +1 -1
- package/templates/example-remesh/evidence/direction/v5.json +1 -1
- package/templates/example-remesh/evidence/direction-input-v4.json +1 -1
- package/templates/example-remesh/evidence/direction-input-v5.json +1 -1
- package/templates/example-remesh/evidence/interview/assemble.json +1 -1
- package/templates/example-remesh/evidence/interview/bones.json +4 -4
- package/templates/example-remesh/evidence/interview/camera_depth.json +2 -2
- package/templates/example-remesh/evidence/interview/constraints.json +4 -4
- package/templates/example-remesh/evidence/interview/meshes.json +4 -4
- package/templates/example-remesh/evidence/interview/motion.json +4 -4
- package/templates/example-remesh/evidence/interview/post_composition.json +2 -2
- package/templates/example-remesh/evidence/interview/post_timing.json +2 -2
- package/templates/example-remesh/evidence/interview/reference.json +1 -1
- package/templates/example-remesh/evidence/interview/rig_type.json +1 -1
- package/templates/example-remesh/evidence/interview/seethrough.json +1 -1
- package/templates/example-remesh/evidence/interview/turn.json +108 -0
- package/templates/example-remesh/evidence/parts-review.json +1 -1
- package/templates/example-remesh/evidence/review-reads/{afe2522be4087f7f2fa7e97746d5f8574b0aedb474245ce2dbce0455e662f041.json → 99ade3eb19d70bd5f727138b3b4876ce86f1e1c22a59b516639bf85593996c32.json} +2 -2
- package/templates/example-remesh/evidence/scene/s1.json +2 -2
- package/templates/example-remesh/evidence/v1/registration/at-source.json +2 -2
- package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +2 -2
- package/templates/example-remesh/evidence/v2/registration/at-source.json +2 -2
- package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +2 -2
- package/templates/example-remesh/evidence/v3/registration/at-source.json +2 -2
- package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +2 -2
- package/templates/example-remesh/outputs/v1/out/check/build/run.json +2 -2
- package/templates/example-remesh/outputs/v1/out/rig/run.json +2 -2
- package/templates/example-remesh/outputs/v2/out/check/build/run.json +2 -2
- package/templates/example-remesh/outputs/v2/out/rig/run.json +2 -2
- package/templates/example-remesh/outputs/v3/out/check/build/run.json +2 -2
- package/templates/example-remesh/outputs/v3/out/rig/run.json +2 -2
- package/templates/example-remesh/project.json +279 -230
- package/templates/example-reopened/project.json +341 -289
- package/templates/example-split/captions.json +1 -1
- package/templates/project.json +10 -2
package/src/job-host.ts
CHANGED
|
@@ -2,17 +2,19 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
-
import { startJobHost, type JobHostAttachFailure, type JobHostOptions, type JobHostReuseBinding, type JobOwnerHost, type JobReviewerBinding, type JobSelector, type
|
|
5
|
+
import { startJobHost, type JobHostAttachFailure, type JobHostOptions, type JobHostReuseBinding, type JobOwnerHost, type JobReviewerBinding, type JobSelector, type JobEventV3, type ActV3 } from '@rig-c/review/job-host';
|
|
6
6
|
import { asArray, checkProject, collectItems, loadCollection, isObject, type Finding } from './check.ts';
|
|
7
7
|
import { baselineOf, newFindings } from './refusal-baseline.ts';
|
|
8
8
|
import { Refusal, refuse } from './refusal.ts';
|
|
9
9
|
import { liveManifestPath, draftPublicationContext, currentJobIdentity, partsEnumerator, prepareDraftManifest, prepareLiveManifest, publicationFile, publicationSelector, readPublished, readRecordPublished } from './job-publication.ts';
|
|
10
|
-
import { acknowledgePublication, acquireJobLock, assertJobIdentity, enqueuePublication, ensureJobPrivateIgnores, failPublication, JOB_BINDING_FILE, jobHash, jobHostStateRoot, pendingPublications, privateDirectory, publicationVersionHash, readAcknowledgedPublication, readWriterBinding, reconcilePublication, writePrivateJson, isRecordResult, type PublicationResult, type JobIdentity, type PendingPublication, type WriterBinding } from './job-outbox.ts';
|
|
10
|
+
import { acknowledgePublication, acquireJobLock, assertJobIdentity, enqueuePublication, ensureJobPrivateIgnores, failPublication, JOB_BINDING_FILE, jobHash, jobHostStateRoot, pendingPublications, privateDirectory, publicationVersionHash, readAcknowledgedPublication, readWriterBinding, reconcilePublication, writePrivateJson, isRecordResult, type PublicationResult, type JobIdentity, type PendingPublication, type WriterBinding, type PublicationSubject } from './job-outbox.ts';
|
|
11
11
|
import { draftPublicationResult } from './job-writer.ts';
|
|
12
12
|
import { attachCreateJobForClose } from './job-close.ts';
|
|
13
13
|
import { pendingJobActs, type PendingJobAct } from './job-acts.ts';
|
|
14
14
|
import { checkReviewPackages } from './review-package.ts';
|
|
15
|
+
import { reviewVerdictSignable } from './signable.ts';
|
|
15
16
|
import type { JsonObject } from './templates.ts';
|
|
17
|
+
import { recoverReplacedJobHostState, replacedRootRefusal } from './job-host-state.ts';
|
|
16
18
|
|
|
17
19
|
function installBinding(root: string, binding: WriterBinding): void {
|
|
18
20
|
ensureJobPrivateIgnores(root);
|
|
@@ -79,10 +81,23 @@ export async function openCreateJob(
|
|
|
79
81
|
if (existing) assertJobIdentity(existing.identity, identity);
|
|
80
82
|
privateDirectory(identity.stateDir);
|
|
81
83
|
let writerGrant: WriterGrant | undefined;
|
|
82
|
-
const
|
|
84
|
+
const start = () => startHost({
|
|
83
85
|
...options(identity, true, undefined, grant => { writerGrant = grant; }),
|
|
84
86
|
...(existing ? { port: Number(new URL(existing.origin).port) } : {}),
|
|
85
87
|
});
|
|
88
|
+
let host: JobOwnerHost;
|
|
89
|
+
try {
|
|
90
|
+
host = await start();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (typeof error !== 'object' || error === null || (error as { code?: unknown }).code !== 'JOB_ROOT_REPLACED') throw error;
|
|
93
|
+
if (identity.stateDir !== join(jobHostStateRoot(root), 'host') || !await recoverReplacedJobHostState(root, now)) replacedRootRefusal(root);
|
|
94
|
+
privateDirectory(identity.stateDir);
|
|
95
|
+
try { host = await start(); }
|
|
96
|
+
catch (retryError) {
|
|
97
|
+
if (typeof retryError === 'object' && retryError !== null && (retryError as { code?: unknown }).code === 'JOB_ROOT_REPLACED') replacedRootRefusal(root);
|
|
98
|
+
throw retryError;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
86
101
|
const file = join(root, 'project.json');
|
|
87
102
|
const temporary = join(root, '.rig-create-job-record.tmp');
|
|
88
103
|
try {
|
|
@@ -174,7 +189,7 @@ export async function publishPending(
|
|
|
174
189
|
const currentHash = jobHash(bytes);
|
|
175
190
|
if (currentHash !== entry.recordHash) { entry.recordHash = currentHash; delete entry.generation; }
|
|
176
191
|
const project = JSON.parse(bytes.toString('utf8'));
|
|
177
|
-
let subjects: readonly
|
|
192
|
+
let subjects: readonly PublicationSubject[];
|
|
178
193
|
if (isRecordResult(entry.result)) {
|
|
179
194
|
subjects = readRecordPublished({ root: entry.root, record: { path: 'project.json', sha256: entry.recordHash }, result: entry.result, phase: 'inventory' }).subjects;
|
|
180
195
|
} else {
|
|
@@ -216,7 +231,7 @@ export async function publishPending(
|
|
|
216
231
|
}
|
|
217
232
|
|
|
218
233
|
/** One declared subject after the ask step: asked now, asked before on this publication, or refused by Review. */
|
|
219
|
-
export type AskOutcome = { readonly subject:
|
|
234
|
+
export type AskOutcome = { readonly subject: PublicationSubject; readonly status: 'asked' | 'already' } | { readonly subject: PublicationSubject; readonly status: 'refused'; readonly code: string };
|
|
220
235
|
|
|
221
236
|
type AgentReply = { ok?: boolean; error?: { code?: string }; value?: { events?: unknown; status?: unknown; cursor?: unknown } };
|
|
222
237
|
|
|
@@ -251,10 +266,10 @@ async function* jobEvents(host: JobOwnerHost, strict = false): AsyncGenerator<Jo
|
|
|
251
266
|
}
|
|
252
267
|
|
|
253
268
|
/** The subjects already asked on this publication, from the same event reader as job acts. */
|
|
254
|
-
async function askedSubjects(host: JobOwnerHost, publicationId: string): Promise<
|
|
255
|
-
const asked:
|
|
269
|
+
async function askedSubjects(host: JobOwnerHost, publicationId: string): Promise<PublicationSubject[]> {
|
|
270
|
+
const asked: PublicationSubject[] = [];
|
|
256
271
|
for await (const event of jobEvents(host)) {
|
|
257
|
-
if (event.type === 'pending' && event.ask.publicationId === publicationId) asked.push(event.ask.subject as
|
|
272
|
+
if (event.type === 'pending' && event.ask.publicationId === publicationId) asked.push(event.ask.subject as PublicationSubject);
|
|
258
273
|
}
|
|
259
274
|
return asked;
|
|
260
275
|
}
|
|
@@ -282,7 +297,7 @@ export async function listCreateJobActs(
|
|
|
282
297
|
* publication, pending or answered, is not asked again, so a re-open never shows a second
|
|
283
298
|
* control. Asks stay limited to the declared list (`REVIEW_ACT_SUBJECT_UNDECLARED`).
|
|
284
299
|
*/
|
|
285
|
-
export async function askDeclaredSubjects(host: JobOwnerHost, publicationId: string, subjects: readonly
|
|
300
|
+
export async function askDeclaredSubjects(host: JobOwnerHost, publicationId: string, subjects: readonly PublicationSubject[]): Promise<AskOutcome[]> {
|
|
286
301
|
const before = await askedSubjects(host, publicationId);
|
|
287
302
|
const outcomes: AskOutcome[] = [];
|
|
288
303
|
for (const subject of subjects) {
|
|
@@ -298,7 +313,7 @@ export async function askDeclaredSubjects(host: JobOwnerHost, publicationId: str
|
|
|
298
313
|
return outcomes;
|
|
299
314
|
}
|
|
300
315
|
|
|
301
|
-
export function subjectLabel(subject:
|
|
316
|
+
export function subjectLabel(subject: PublicationSubject): string {
|
|
302
317
|
switch (subject.kind) {
|
|
303
318
|
case 'parts-verdict': return `parts-verdict ${subject.draftId}`;
|
|
304
319
|
case 'checkpoint': return `checkpoint ${subject.checkpointId}`;
|
|
@@ -325,11 +340,16 @@ export function refuseAskOutcomes(outcomes: readonly AskOutcome[]): void {
|
|
|
325
340
|
refuse('JOB_ASK_REFUSED', `project.json: the publication is acknowledged but review refused the ask for ${named}; restore the published record bytes and run rig-create job open to ask again`);
|
|
326
341
|
}
|
|
327
342
|
|
|
328
|
-
export async function reconcileCreateJob(root: string, publish: (entry: PendingPublication) => Promise<unknown> = publishPending): Promise<number> {
|
|
343
|
+
export async function reconcileCreateJob(root: string, publish: (entry: PendingPublication) => Promise<unknown> = publishPending, now?: number): Promise<number> {
|
|
344
|
+
root = realpathSync(root);
|
|
329
345
|
const release = acquireJobLock(root)!;
|
|
330
346
|
let outcomes: AskOutcome[];
|
|
331
347
|
let count: number;
|
|
332
|
-
try {
|
|
348
|
+
try {
|
|
349
|
+
const binding = readWriterBinding(root);
|
|
350
|
+
if (!binding || binding.identity.stateDir === join(jobHostStateRoot(root), 'host')) await recoverReplacedJobHostState(root, now);
|
|
351
|
+
({ count, outcomes } = await reconcileLocked(root, publish));
|
|
352
|
+
}
|
|
333
353
|
finally { release(); }
|
|
334
354
|
refuseAskOutcomes(outcomes);
|
|
335
355
|
return count;
|
|
@@ -482,13 +502,16 @@ export async function openDraftPublication(root: string, host: JobOwnerHost, now
|
|
|
482
502
|
? headSelector
|
|
483
503
|
: undefined);
|
|
484
504
|
const warnings: Finding[] = [];
|
|
505
|
+
const project = JSON.parse(bytes.toString('utf8')) as JsonObject;
|
|
485
506
|
const draftContext = targetSelector?.kind === 'draft'
|
|
486
|
-
? draftPublicationContext(root,
|
|
507
|
+
? draftPublicationContext(root, project, targetSelector.id, warnings)
|
|
487
508
|
: undefined;
|
|
488
509
|
// Re-publish an older post head that still declares a sign-off or checkpoint:
|
|
489
510
|
// either pending ask would prevent the legacy direction panel from rendering.
|
|
490
511
|
const currentSubjects = draftContext?.subjects ?? (targetSelector?.kind === 'post'
|
|
491
|
-
?
|
|
512
|
+
? (reviewVerdictSignable(project, targetSelector.version, 'direction-review')
|
|
513
|
+
? [{ kind: 'review-verdict' as const, stage: 'direction-review' as const }]
|
|
514
|
+
: [])
|
|
492
515
|
: undefined);
|
|
493
516
|
const reuseHead = Boolean(head && headSelector && source?.projectHash === jobHash(bytes) && (
|
|
494
517
|
targetSelector === undefined || isDeepStrictEqual(headSelector, targetSelector)
|
package/src/job-outbox.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { existsSync, JOB_PRIVATE_DIRECTORY, lstatSync, readFileSync, readdirSync
|
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
-
import type { JobPublishRequest, JobReviewerBinding, JobSubject, RecordResult } from '@rig-c/review/job-host';
|
|
7
|
+
import type { JobPublishRequest, JobReviewerBinding, JobSubject, RecordSubject, RecordResult } from '@rig-c/review/job-host';
|
|
8
|
+
|
|
9
|
+
export type PublicationSubject = JobSubject | RecordSubject;
|
|
8
10
|
import { refuse } from './refusal.ts';
|
|
9
11
|
|
|
10
12
|
export const JOB_BINDING_FILE = '.rig-create-review-binding.json';
|
|
@@ -342,7 +344,7 @@ export interface AcknowledgedPublication {
|
|
|
342
344
|
result: PendingPublication['result'];
|
|
343
345
|
versionHash: string;
|
|
344
346
|
/** The subjects the published context declared. Receipts written before they were retained omit it. */
|
|
345
|
-
subjects?: readonly
|
|
347
|
+
subjects?: readonly PublicationSubject[];
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
function acknowledgedPath(root: string, jobId: string, publicationId: string): string {
|
|
@@ -350,7 +352,7 @@ function acknowledgedPath(root: string, jobId: string, publicationId: string): s
|
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
/** Persist the version identity before the pending publication is removed. */
|
|
353
|
-
export function acknowledgePublication(entry: PendingPublication, publicationId: string, generation: number, requestId: string, subjects?: readonly
|
|
355
|
+
export function acknowledgePublication(entry: PendingPublication, publicationId: string, generation: number, requestId: string, subjects?: readonly PublicationSubject[]): void {
|
|
354
356
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(publicationId) || !Number.isSafeInteger(generation) || generation < 0 || !entry.versionHash) {
|
|
355
357
|
refuse('JOB_OUTBOX_INVALID', 'project.json: publication acknowledgment lacks its identity; reconcile the original pending publication');
|
|
356
358
|
}
|
package/src/job-publication.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { checkInputs } from './inputs.ts';
|
|
2
2
|
import { pngDimensions } from './scene.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { hasPartsReceipt } from './fix-ledger.ts';
|
|
4
4
|
import { renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { existsSync, lstatSync, readFileSync, realpathSync } from './check-reads.ts';
|
|
6
6
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
7
7
|
import { isDeepStrictEqual } from 'node:util';
|
|
8
8
|
import { partsFileReferences } from '@rig-c/parts/src/parts.ts';
|
|
9
9
|
import partsPackage from '@rig-c/parts/package.json';
|
|
10
|
-
import { JOB_ACT_SCHEMA, JOB_API_SCHEMA, type JobPublicationReader, type JobPublishedContext, type JobSelector, type JobRecordSignoffStage } from '@rig-c/review/job-host';
|
|
10
|
+
import { JOB_ACT_SCHEMA, JOB_API_SCHEMA, type JobPublicationReader, type JobPublishedContext, type JobSelector, type JobRecordSignoffStage, type RecordPlanSubject } from '@rig-c/review/job-host';
|
|
11
11
|
import { stepHash } from './step-hash.ts';
|
|
12
12
|
import { checkpointEvidenceContext, type CheckpointEvidence } from './checkpoint-evidence.ts';
|
|
13
13
|
import { locateParts, PARTS_PACKAGE } from './parts-sections.ts';
|
|
@@ -24,10 +24,10 @@ import {
|
|
|
24
24
|
import { optionContextFiles, optionFile, validateContext } from './choices.ts';
|
|
25
25
|
import { hasStageOrder, interviewStageOfGuide, stageOfStep, stepCoveredByApproval } from './stages.ts';
|
|
26
26
|
import { selectAnswerHashes } from './signoff-answers.ts';
|
|
27
|
-
import { assertInterviewRecordSignable, packageSignSubjects, stageSignable } from './signable.ts';
|
|
28
|
-
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
27
|
+
import { assertInterviewRecordSignable, checkpointSignable, packageSignSubjects, reviewVerdictSignable, stageSignable } from './signable.ts';
|
|
28
|
+
import { collectReopens, directionReviewClosed, liveSignoff } from './reopen.ts';
|
|
29
29
|
import { acceptedReviewVerdict } from './review.ts';
|
|
30
|
-
import { requireRecordDirectionTarget } from './record-direction-target.ts';
|
|
30
|
+
import { recordDirectionTarget, requireRecordDirectionTarget } from './record-direction-target.ts';
|
|
31
31
|
import { latestPartsApproval, riggingContext } from './structure.ts';
|
|
32
32
|
import { jobHash, pendingPublications, isRecordResult, type JobIdentity } from './job-outbox.ts';
|
|
33
33
|
import { Refusal, refuse } from './refusal.ts';
|
|
@@ -57,8 +57,6 @@ type RecordContextBase = {
|
|
|
57
57
|
readonly files: JobPublishedContext['files'];
|
|
58
58
|
};
|
|
59
59
|
type RecordSignoffStage = JobRecordSignoffStage;
|
|
60
|
-
type RecordPlanSubject = { readonly kind: 'checkpoint'; readonly checkpointId: string }
|
|
61
|
-
| { readonly kind: 'signoff'; readonly stage: RecordSignoffStage };
|
|
62
60
|
export type RecordOptionContext = RecordContextBase & {
|
|
63
61
|
readonly plan: null;
|
|
64
62
|
readonly subjects: readonly [{ readonly kind: 'interview-picks'; readonly questionId: string; readonly setId: string }];
|
|
@@ -285,7 +283,9 @@ export function readRecordPublished({ root, record, result, phase, parts: sealed
|
|
|
285
283
|
label = `option-set ${result.questionId} / ${result.setId}`;
|
|
286
284
|
} else if (result.kind === 'plan') {
|
|
287
285
|
if (!plan) throw Error('the requested plan is absent');
|
|
288
|
-
|
|
286
|
+
const reopenDirection = stage === 'direction-review' && directionReviewClosed(project)
|
|
287
|
+
&& recordDirectionTarget(project, now) === null;
|
|
288
|
+
if (stage === 'direction-review' && !reopenDirection) {
|
|
289
289
|
const reopens = collectReopens(project, []);
|
|
290
290
|
const post = asArray(project.versions).filter(isObject).filter(version => version.kind === 'post'
|
|
291
291
|
&& liveSignoff(reopens, 'post', Date.parse(String(version.at)))).at(-1);
|
|
@@ -297,7 +297,7 @@ export function readRecordPublished({ root, record, result, phase, parts: sealed
|
|
|
297
297
|
requireRecordDirectionTarget(project, now);
|
|
298
298
|
}
|
|
299
299
|
const covered = steps.filter(step => stepCoveredByApproval(project, step, stage));
|
|
300
|
-
const signable = stage !== null && ['interview', 'plan-approved', 'rigging-interview', 'post-interview', 'direction-review'].includes(stage)
|
|
300
|
+
const signable = !reopenDirection && stage !== null && ['interview', 'plan-approved', 'rigging-interview', 'post-interview', 'direction-review'].includes(stage)
|
|
301
301
|
&& (stage !== 'interview' || interviewSignoff) && stageSignable(project, stage);
|
|
302
302
|
if (signable && stage === 'direction-review' && steps.length === 0) throw Error('direction-review record sign-off needs displayed plan steps');
|
|
303
303
|
const emptyPost = stage === 'post-interview' && !steps.some(step => step.phase === 'post') && postNotPassed({ items });
|
|
@@ -311,9 +311,10 @@ export function readRecordPublished({ root, record, result, phase, parts: sealed
|
|
|
311
311
|
}
|
|
312
312
|
subjects = steps.filter(step => {
|
|
313
313
|
if (step.status !== 'at-checkpoint' || (step.phase !== 'rig' && step.phase !== 'post') || stageOfStep(step.phase, String(step.id)) !== stage) return false;
|
|
314
|
-
return
|
|
314
|
+
return checkpointSignable(project, stage, String(step.id), now);
|
|
315
315
|
}).map(step => ({ kind: 'checkpoint' as const, checkpointId: String(step.id) }));
|
|
316
316
|
if (signable) subjects = [...subjects, { kind: 'signoff', stage: stage as RecordSignoffStage }];
|
|
317
|
+
if (reopenDirection) subjects = [{ kind: 'reopen', stage: 'direction-review' }];
|
|
317
318
|
label = `plan / ${stage}`;
|
|
318
319
|
} else throw Error('unknown record result');
|
|
319
320
|
// Detect record changes during closure reads as well as changes before entry.
|
|
@@ -513,9 +514,12 @@ export const readPublished: JobPublicationReader = async ({ root, record, result
|
|
|
513
514
|
}
|
|
514
515
|
const plan = project.plan as JsonObject | undefined;
|
|
515
516
|
const steps = Array.isArray(plan?.steps) ? plan.steps as JsonObject[] : [];
|
|
516
|
-
const
|
|
517
|
+
const reviewStage = selector.kind === 'post' ? 'direction-review' as const : 'rig-review' as const;
|
|
518
|
+
const subjects: JobPublishedContext['subjects'] = [
|
|
519
|
+
...(reviewVerdictSignable(project, selector.version, reviewStage) ? [{ kind: 'review-verdict' as const, stage: reviewStage }] : []),
|
|
517
520
|
// The legacy direction panel accepts only the verdict on a post publication.
|
|
518
|
-
...(selector.kind === 'post' ? [] : packageSignSubjects(project, effectiveStage(root, project), selector, steps.length))
|
|
521
|
+
...(selector.kind === 'post' ? [] : packageSignSubjects(project, effectiveStage(root, project), selector, steps.length)),
|
|
522
|
+
];
|
|
519
523
|
return {
|
|
520
524
|
selector, plan: { steps: steps.map(step => ({ id: String(step.id), hash: stepHash(step) })) },
|
|
521
525
|
parts: { enumeratorVersion: partsEnumerator.version, manifest: { path: manifestPath, sha256: jobHash(manifest) }, images },
|
|
@@ -576,7 +580,7 @@ export function draftPublishedContext(root: string, project: JsonObject, id: str
|
|
|
576
580
|
const plan = project.plan as JsonObject | undefined;
|
|
577
581
|
return { selector: { kind: 'draft', id }, plan: plan ? { steps: (plan.steps as JsonObject[] ?? []).map(step => ({ id: String(step.id), hash: stepHash(step) })) } : null,
|
|
578
582
|
parts: { enumeratorVersion: partsEnumerator.version, manifest: { path: manifestPath, sha256: jobHash(manifest) }, images },
|
|
579
|
-
subjects: [{ kind: 'parts-verdict', draftId: id }],
|
|
583
|
+
subjects: hasPartsReceipt(project, id) ? [] : [{ kind: 'parts-verdict', draftId: id }],
|
|
580
584
|
partIds: (JSON.parse(manifest.toString('utf8')).parts as { name: string }[]).map(part => part.name), files };
|
|
581
585
|
}
|
|
582
586
|
|
package/src/job-runtime.ts
CHANGED
|
@@ -5,6 +5,16 @@ import { askOutcomeLines, assertRequestedDraft, assertRequestedRig, closeCreateJ
|
|
|
5
5
|
import { failPublication, pendingPublications, reconcilePublication } from './job-outbox.ts';
|
|
6
6
|
import { jobActsText } from './job-acts.ts';
|
|
7
7
|
import { Refusal, refuse } from './refusal.ts';
|
|
8
|
+
import { replacedRootRefusal } from './job-host-state.ts';
|
|
9
|
+
|
|
10
|
+
/** Preserve this known producer refusal without printing an uncontrolled producer message. */
|
|
11
|
+
export function publicationFailure(error: unknown, root: string): Refusal | undefined {
|
|
12
|
+
if (error instanceof Refusal) return error;
|
|
13
|
+
if (typeof error === 'object' && error !== null && (error as { code?: unknown }).code === 'JOB_ROOT_REPLACED') {
|
|
14
|
+
try { replacedRootRefusal(root); } catch (failure) { return failure as Refusal; }
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
async function run(): Promise<void> {
|
|
10
20
|
const input = JSON.parse(readFileSync(0, 'utf8')) as { root: string; id?: string; reviewer?: string; role?: 'owner' | 'delegate'; draft?: string; rig?: string; question?: string; plan?: boolean; now: number; json?: boolean };
|
|
@@ -16,7 +26,8 @@ async function run(): Promise<void> {
|
|
|
16
26
|
let outcomes: AskOutcome[] = [];
|
|
17
27
|
try { await reconcilePublication(root, entry, async item => { outcomes = await publishPending(item); }); }
|
|
18
28
|
catch (error) {
|
|
19
|
-
const
|
|
29
|
+
const named = publicationFailure(error, root);
|
|
30
|
+
const failure = named ? { code: named.code, message: named.message }
|
|
20
31
|
: { code: 'JOB_PUBLISH_PENDING', message: 'project.json: host publication failed; restore the bound host before rig-create job reconcile' };
|
|
21
32
|
failPublication(root, entry, failure);
|
|
22
33
|
throw new Refusal('JOB_PUBLISH_PENDING', 'project.json: committed publication is pending; run rig-create job reconcile');
|
|
@@ -31,7 +42,7 @@ async function run(): Promise<void> {
|
|
|
31
42
|
console.log(input.json ? JSON.stringify(result) : jobActsText(result));
|
|
32
43
|
break;
|
|
33
44
|
}
|
|
34
|
-
case 'reconcile': console.log(`job: reconciled ${await reconcileCreateJob(root)} publication(s)`); break;
|
|
45
|
+
case 'reconcile': console.log(`job: reconciled ${await reconcileCreateJob(root, undefined, input.now)} publication(s)`); break;
|
|
35
46
|
case 'close': await closeCreateJob(root); console.log('job: closed'); break;
|
|
36
47
|
case 'open': {
|
|
37
48
|
if (!input.reviewer || !input.role) refuse('JOB_BINDING_INVALID', 'project.json: job open needs --reviewer and --role owner|delegate');
|
|
@@ -64,7 +75,8 @@ async function run(): Promise<void> {
|
|
|
64
75
|
host.stop();
|
|
65
76
|
throw error;
|
|
66
77
|
}
|
|
67
|
-
|
|
78
|
+
const named = publicationFailure(error, root);
|
|
79
|
+
console.error(named ? `${named.code}: ${named.message}` : 'JOB_PUBLISH_PENDING: project.json: draft publication failed; run rig-create job reconcile');
|
|
68
80
|
process.exitCode = 1;
|
|
69
81
|
}
|
|
70
82
|
if (host.reused || stopping) {
|
|
@@ -93,7 +105,8 @@ if (import.meta.main) {
|
|
|
93
105
|
try { await run(); }
|
|
94
106
|
catch (error) {
|
|
95
107
|
// Unexpected errors may embed producer input. Only controlled refusals leave this process.
|
|
96
|
-
|
|
108
|
+
const named = publicationFailure(error, 'the requested project directory');
|
|
109
|
+
console.error(named ? `${named.code}: ${named.message}` : 'JOB_PUBLISH_PENDING: project.json: publication operation failed; run rig-create job reconcile');
|
|
97
110
|
process.exitCode = 1;
|
|
98
111
|
}
|
|
99
112
|
}
|
package/src/next.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { pausedFrom } from './pause.ts';
|
|
|
32
32
|
import { carriedRefusals, findingKey, newFindings } from './refusal-baseline.ts';
|
|
33
33
|
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
34
34
|
import { reopenLapseWarnings } from './reopen-lapses.ts';
|
|
35
|
-
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
35
|
+
import { collectReopens, directionReviewClosed, liveSignoff } from './reopen.ts';
|
|
36
36
|
import { acceptedReviewVerdict } from './review.ts';
|
|
37
37
|
import {
|
|
38
38
|
advanceOrder,
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
} from './stages.ts';
|
|
55
55
|
import type { Json, JsonObject } from './templates.ts';
|
|
56
56
|
import { sceneCurrentAt, sceneRig, SceneError } from './scene.ts';
|
|
57
|
+
import { depthPlanMoves } from './rig-version.ts';
|
|
57
58
|
import { resolveAgentCitationSpec } from './scene-authority.ts';
|
|
58
59
|
|
|
59
60
|
export const NEXT_SCHEMA = 'rig-create/next@1';
|
|
@@ -72,7 +73,7 @@ export interface NextRefusal {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
export interface NextReviewAct {
|
|
75
|
-
kind: 'parts-verdict' | 'interview-picks' | 'review-verdict' | 'signoff' | 'signoff-checkpoint';
|
|
76
|
+
kind: 'parts-verdict' | 'interview-picks' | 'review-verdict' | 'signoff' | 'signoff-checkpoint' | 'reopen';
|
|
76
77
|
stage: string;
|
|
77
78
|
draft_id?: string;
|
|
78
79
|
question_id?: string;
|
|
@@ -209,7 +210,10 @@ function reviewCommands(dir: string, project: JsonObject, move: NextMove): NextM
|
|
|
209
210
|
if (move.role !== 'human' || move.unanswered_by_record || move.field === 'pauses' || !act) return [move];
|
|
210
211
|
let selector: string;
|
|
211
212
|
let writer: string;
|
|
212
|
-
if (act.kind === '
|
|
213
|
+
if (act.kind === 'reopen') {
|
|
214
|
+
selector = '--plan';
|
|
215
|
+
writer = `reopen ${act.stage}`;
|
|
216
|
+
} else if (act.kind === 'interview-picks' && act.question_id) {
|
|
213
217
|
selector = `--question ${shellWord(act.question_id)}`;
|
|
214
218
|
writer = `interview-answer ${shellWord(act.question_id)}`;
|
|
215
219
|
} else if (act.kind === 'signoff' && ['interview', 'plan-approved', 'rigging-interview', 'post-interview', 'direction-review'].includes(act.stage)) {
|
|
@@ -691,18 +695,12 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
691
695
|
if (kind === 'post') {
|
|
692
696
|
// Match the direction writer's authoring gate, including earlier
|
|
693
697
|
// accepted verdicts that a later bounce does not itself reopen.
|
|
694
|
-
|
|
695
|
-
const closed = collectSignoffs(project, []).some(s => s.stage === 'direction-review'
|
|
696
|
-
&& s.checkpoint === null && liveSignoff(reopens, s.stage, s.at))
|
|
697
|
-
|| versions.some(v => v.kind === 'post' && asArray(v.verdicts).filter(isObject).some(d =>
|
|
698
|
-
d.review === 'direction-review' && d.verdict === 'accepted'
|
|
699
|
-
&& liveSignoff(reopens, 'direction-review', Date.parse(String(d.at)))));
|
|
700
|
-
if (closed) {
|
|
698
|
+
if (directionReviewClosed(project)) {
|
|
701
699
|
const target = 'reopens[stage=direction-review]';
|
|
702
700
|
addAwait(target);
|
|
703
701
|
moves.push({ role: 'human', field: 'reopens', awaits: target,
|
|
704
|
-
command:
|
|
705
|
-
act: '
|
|
702
|
+
command: null, review_act: { kind: 'reopen', stage: 'direction-review' },
|
|
703
|
+
act: 'reopen direction-review in Review, then ingest the exported reopen act before writing the replacement post' });
|
|
706
704
|
continue;
|
|
707
705
|
}
|
|
708
706
|
}
|
|
@@ -716,7 +714,7 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
716
714
|
command: `rig-create ${writer} ${shellWord(dir)} ${inputs.replace(/<[^>]+>/g, shellWord)} ${refs.map(r => `--answers ${r.version}:${r.verdict}`).join(' ')}`,
|
|
717
715
|
act: `build the next ${kind} version with ${writer}, recording answers_feedback ${JSON.stringify(refs)}` });
|
|
718
716
|
}
|
|
719
|
-
if (moves.length > 0) return { schema: NEXT_SCHEMA, project_id: String(project.name ?? ''), stage, effective_stage: effectiveStage, accepted: false, refusals, ...warningFields, awaits: awaitsList, moves };
|
|
717
|
+
if (moves.length > 0) return { schema: NEXT_SCHEMA, project_id: String(project.name ?? ''), stage, effective_stage: effectiveStage, accepted: false, refusals, ...warningFields, awaits: awaitsList, moves: moves.flatMap(move => reviewCommands(dir, project, move)) };
|
|
720
718
|
}
|
|
721
719
|
const recordedLapseTargets = new Set<string>();
|
|
722
720
|
const steps = asArray(isObject(project.plan) ? project.plan.steps : undefined).filter(isObject);
|
|
@@ -873,6 +871,12 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
873
871
|
const splitOrder = hasStageOrder(project);
|
|
874
872
|
const drafts = asArray(project.drafts).filter(isObject);
|
|
875
873
|
const versions = asArray(project.versions).filter(isObject);
|
|
874
|
+
if (effectiveStage === 'rigging-interview' && depthPlanMoves(project, collection).length > 0
|
|
875
|
+
&& !steps.some(s => s.id === 'turn' && s.phase === 'rig')) {
|
|
876
|
+
addAwait('plan.steps[turn]');
|
|
877
|
+
moves.push({ role: 'agent', field: 'plan.steps[turn]', awaits: 'plan.steps[turn]', command: null,
|
|
878
|
+
act: 'Draft plan add turn --phase rig --rests-on turn before the rig steps, record the chosen depth route and reason in its text, with a bar that reads the producer depth reports; approve it at rigging-interview.' });
|
|
879
|
+
}
|
|
876
880
|
|
|
877
881
|
if (stage === PAUSED_STAGE) {
|
|
878
882
|
const resumeTarget = pausedFrom(project);
|
|
@@ -1235,6 +1239,10 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
1235
1239
|
}
|
|
1236
1240
|
}
|
|
1237
1241
|
} else if (effectiveStage === 'rigging' || effectiveStage === 'rig-review') {
|
|
1242
|
+
if (effectiveStage === 'rigging') for (const move of depthPlanMoves(project, collection)) {
|
|
1243
|
+
addAwait(move.awaits);
|
|
1244
|
+
moves.push(move);
|
|
1245
|
+
}
|
|
1238
1246
|
const rigSteps = steps.filter(
|
|
1239
1247
|
s => (s.phase === 'rig' || s.phase === 'post') && stageOfStep(s.phase, String(s.id)) === 'rigging',
|
|
1240
1248
|
);
|
package/src/parts-review.ts
CHANGED
|
@@ -3,12 +3,11 @@ import { assertPartsCliArgs, assertPartsReadDraft, decodePartsRead, partsReadFin
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
6
|
import { readProject } from './feedback.ts';
|
|
8
7
|
import { readPartsReview, ReviewReadError, storeReviewRead, type ReviewReadRunner } from './review-read.ts';
|
|
9
|
-
import { acquireBoundJobWrite
|
|
8
|
+
import { acquireBoundJobWrite } from './job-writer.ts';
|
|
10
9
|
import { acquireJobLock, jobHash, readAcknowledgedPublication, readWriterBinding } from './job-outbox.ts';
|
|
11
|
-
import { decodeReviewAct, reviewActFindings, reviewActTransport, reviewActVerbatim } from './review-act.ts';
|
|
10
|
+
import { decodeReviewAct, fetchReviewActBytes, reviewActFindings, reviewActTransport, reviewActVerbatim } from './review-act.ts';
|
|
12
11
|
import { assertPartsActSource, partsActDraftHash, partsActFindings, partsActReceipt } from './parts-act.ts';
|
|
13
12
|
import { newRefusals } from './refusal-baseline.ts';
|
|
14
13
|
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
@@ -58,14 +57,8 @@ export function partsReviewCommand(args: string[], now: number, hooks: PartsRevi
|
|
|
58
57
|
let committed = false;
|
|
59
58
|
const temporary = join(root, `.parts-review-${randomUUID()}.tmp`);
|
|
60
59
|
try {
|
|
61
|
-
const transport = { ...reviewActTransport(root, actId!), kind: 'parts-verdict' };
|
|
62
|
-
const
|
|
63
|
-
stdin: Buffer.from(JSON.stringify(transport)), stdout: 'pipe', stderr: 'pipe',
|
|
64
|
-
});
|
|
65
|
-
let response: { ok: boolean; bytes: string; code: string; message: string };
|
|
66
|
-
try { response = JSON.parse(child.stdout.toString()); }
|
|
67
|
-
catch { return refuse('REVIEW_ACT_FETCH_REFUSED', 'project.json.review_acts: fetch adapter failed; restore the live Review job'); }
|
|
68
|
-
if (!response.ok || child.exitCode) refuse(response.code, response.message);
|
|
60
|
+
const transport = { ...reviewActTransport(root, actId!), kind: 'parts-verdict' as const };
|
|
61
|
+
const response = { bytes: fetchReviewActBytes(transport) };
|
|
69
62
|
const act = decodeReviewAct(response.bytes, 'parts-verdict');
|
|
70
63
|
if (Date.parse(act.at) > now) refuse('REVIEW_ACT_INVALID', 'project.json.review_acts: act is dated in the future; restore the clock before recording');
|
|
71
64
|
const { file } = readProject(root);
|
package/src/reopen.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync } from './check-reads.ts';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
4
|
import { resolve } from 'node:path';
|
|
3
5
|
|
|
4
|
-
import { asArray, asString, isObject, type Finding } from './check.ts';
|
|
6
|
+
import { asArray, asString, collectSignoffs, isObject, type Finding } from './check.ts';
|
|
5
7
|
import { collectAmendments } from './amend.ts';
|
|
6
8
|
import { readProject } from './feedback.ts';
|
|
9
|
+
import { jobHash } from './job-outbox.ts';
|
|
10
|
+
import { acquireBoundJobWrite } from './job-writer.ts';
|
|
11
|
+
import { assertActSubjectDeclared, decodeReviewAct, fetchReviewActBytes, reviewActFindings, reviewActTransport, type ReviewActTransport, type ReopenAct } from './review-act.ts';
|
|
12
|
+
import { stepHash } from './step-hash.ts';
|
|
7
13
|
import { readVerbatim } from './record-input.ts';
|
|
8
14
|
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
9
15
|
import { newRefusals } from './refusal-baseline.ts';
|
|
@@ -77,6 +83,16 @@ function reachedIndex(project: JsonObject, at: number, fromStage: string): numbe
|
|
|
77
83
|
return reached;
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
/** An acceptance remains authoring authority until an explicit reopen supersedes it. */
|
|
87
|
+
export function directionReviewClosed(project: JsonObject): boolean {
|
|
88
|
+
const reopens = collectReopens(project, []);
|
|
89
|
+
return collectSignoffs(project, []).some(signoff => signoff.stage === 'direction-review'
|
|
90
|
+
&& signoff.checkpoint === null && liveSignoff(reopens, signoff.stage, signoff.at))
|
|
91
|
+
|| asArray(project.versions).filter(isObject).some(version => version.kind === 'post'
|
|
92
|
+
&& asArray(version.verdicts).filter(isObject).some(verdict => verdict.review === 'direction-review'
|
|
93
|
+
&& verdict.verdict === 'accepted' && liveSignoff(reopens, 'direction-review', Date.parse(String(verdict.at)))));
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
/** Each reopen invalidates its suffix; a later, narrower reopen cannot resurrect an earlier suffix. */
|
|
81
97
|
export function reopenTime(reopens: readonly Reopen[], stage: string): number {
|
|
82
98
|
return Math.max(-Infinity, ...reopens.filter(r => stageIndex(r.stage) <= stageIndex(stage)).map(r => r.at));
|
|
@@ -94,38 +110,77 @@ export function liveSignoff(reopens: readonly Reopen[], stage: string, at: numbe
|
|
|
94
110
|
|
|
95
111
|
export const REOPEN_HELP = `reopen:
|
|
96
112
|
<stage> [<project dir>] record a human backward move and set project.stage
|
|
113
|
+
--from-review <actId> ingest the exported Review reopen (excludes --by, --verbatim-file and --source)
|
|
97
114
|
--by <who> human signer (agent is refused)
|
|
98
115
|
--verbatim-file <file> unchanged UTF-8 words; relative to the current working directory, not the project; or - for stdin (default)
|
|
99
116
|
--source <text> optional origin of the words, separate from verbatim
|
|
100
117
|
--json print the reopen record
|
|
101
118
|
The clock supplies at; sign-offs and versions remain history.`;
|
|
102
119
|
|
|
103
|
-
export function reopenCommand(args: string[], now: number): { stdout: string; exit: number } {
|
|
120
|
+
export function reopenCommand(args: string[], now: number, hooks: { fetch?: (transport: ReviewActTransport) => { bytes: string } } = {}): { stdout: string; exit: number } {
|
|
104
121
|
const opts: Record<string, string> = {};
|
|
105
122
|
const positional: string[] = [];
|
|
106
123
|
let json = false;
|
|
107
124
|
for (let i = 0; i < args.length; i++) {
|
|
108
125
|
const arg = args[i] as string;
|
|
109
126
|
if (arg === '--json') json = true;
|
|
110
|
-
else if (['--by', '--verbatim-file', '--source'].includes(arg)) {
|
|
127
|
+
else if (['--by', '--verbatim-file', '--source', '--from-review'].includes(arg)) {
|
|
111
128
|
const value = args[++i];
|
|
112
129
|
if (arg in opts || value === undefined || value === '') refuse('REOPEN_OPTION_INVALID', `reopens: ${arg} needs one non-empty value; supply it once`, USAGE_EXIT);
|
|
113
130
|
opts[arg] = value;
|
|
114
131
|
} else if (arg.startsWith('-')) refuse('UNKNOWN_OPTION', `reopens: unsupported ${arg}; see reopen help`, USAGE_EXIT);
|
|
115
132
|
else positional.push(arg);
|
|
116
133
|
}
|
|
117
|
-
|
|
134
|
+
const actId = opts['--from-review'];
|
|
135
|
+
if (positional.length < 1 || positional.length > 2 || actId === undefined && opts['--by'] === undefined) refuse('REOPEN_OPTION_INVALID', 'reopens: supply a stage, --by or --from-review, and one optional project directory', USAGE_EXIT);
|
|
136
|
+
if (actId !== undefined && ['--by', '--verbatim-file', '--source'].some(flag => opts[flag] !== undefined)) {
|
|
137
|
+
refuse('REOPEN_OPTION_INVALID', 'project.json.reopens: --from-review excludes --by, --verbatim-file and --source; use the exported human values', USAGE_EXIT);
|
|
138
|
+
}
|
|
118
139
|
const dir = resolve(positional[1] ?? process.cwd());
|
|
140
|
+
const release = actId === undefined ? () => {} : acquireBoundJobWrite(dir);
|
|
141
|
+
try { return recordReopen(dir, positional[0]!, opts, json, now, hooks); }
|
|
142
|
+
finally { release(); }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function recordReopen(dir: string, stage: string, opts: Record<string, string>, json: boolean, now: number,
|
|
146
|
+
hooks: { fetch?: (transport: ReviewActTransport) => { bytes: string } }): { stdout: string; exit: number } {
|
|
119
147
|
const { file, project } = readProject(dir);
|
|
148
|
+
const original = readFileSync(file);
|
|
149
|
+
let review: { act: ReopenAct; bytes: string } | undefined;
|
|
150
|
+
const result = (entry: JsonObject): { stdout: string; exit: number } => ({ stdout: json ? JSON.stringify(entry, null, 2) : `reopen: ${String(entry.stage)} by ${String(entry.by)} at ${String(entry.at)}`, exit: 0 });
|
|
151
|
+
const actId = opts['--from-review'];
|
|
152
|
+
if (actId !== undefined) {
|
|
153
|
+
const invalid = reviewActFindings(project)[0];
|
|
154
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
155
|
+
const transport: ReviewActTransport = { ...reviewActTransport(dir, actId), kind: 'reopen' };
|
|
156
|
+
const bytes = hooks.fetch ? hooks.fetch(transport).bytes : fetchReviewActBytes(transport);
|
|
157
|
+
const act = decodeReviewAct(bytes, 'reopen');
|
|
158
|
+
if (act.jobId !== transport.jobId || act.actId !== actId || Date.parse(act.at) > now) {
|
|
159
|
+
refuse('REVIEW_ACT_INVALID', 'project.json.reopens: fetched act identity or date differs from the request; request the correct reopen act');
|
|
160
|
+
}
|
|
161
|
+
if (act.value.stage !== stage) refuse('REVIEW_ACT_KIND_MISMATCH', `project.json.reopens: act reopens ${act.value.stage}, not ${stage}; invoke reopen for the exported stage`);
|
|
162
|
+
const existing = asArray(project.review_acts).filter(isObject).find(entry => entry.jobId === act.jobId && entry.actId === actId);
|
|
163
|
+
if (existing) {
|
|
164
|
+
if (existing.kind !== 'reopen' || existing.bytes !== bytes) refuse('REVIEW_ACT_REPLAY_CONFLICT', 'project.json.review_acts: immutable reopen act changed; restore the original receipt');
|
|
165
|
+
return result(asArray(project.reopens)[Number(existing.reopen)] as JsonObject);
|
|
166
|
+
}
|
|
167
|
+
if (act.source.projectHash !== jobHash(original) || !isDeepStrictEqual(act.source.plan.steps,
|
|
168
|
+
asArray(isObject(project.plan) ? project.plan.steps : undefined).map(raw => ({ id: isObject(raw) ? raw.id : null, hash: isObject(raw) ? stepHash(raw) : null })))) {
|
|
169
|
+
refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.reopens: displayed record or plan changed; request a reopen on the current record');
|
|
170
|
+
}
|
|
171
|
+
assertActSubjectDeclared(dir, act, 'project.json.reopens');
|
|
172
|
+
review = { act, bytes };
|
|
173
|
+
}
|
|
120
174
|
const before = structuredClone(project);
|
|
121
175
|
const steps = asArray(isObject(project.plan) ? project.plan.steps : undefined);
|
|
122
176
|
if (steps.some(s => !isObject(s) || typeof s.id !== 'string' || (s.phase !== 'rig' && s.phase !== 'post'))) {
|
|
123
177
|
refuse('REOPEN_MALFORMED', 'plan.steps: each step needs id and phase; restore the plan before reopening');
|
|
124
178
|
}
|
|
125
179
|
const affected = steps.filter((s): s is JsonObject => isObject(s)
|
|
126
|
-
&& stageIndex(stageOfStep(s.phase as 'rig' | 'post', s.id as string)) >= stageIndex(
|
|
127
|
-
const entry: JsonObject = { from_stage: project.stage ?? null, steps_before: affected.map(s => ({ id: s.id as string, ...(s.status === undefined ? {} : { status: s.status }) })), stage
|
|
128
|
-
if (
|
|
180
|
+
&& stageIndex(stageOfStep(s.phase as 'rig' | 'post', s.id as string)) >= stageIndex(stage));
|
|
181
|
+
const entry: JsonObject = { from_stage: project.stage ?? null, steps_before: affected.map(s => ({ id: s.id as string, ...(s.status === undefined ? {} : { status: s.status }) })), stage, by: review?.act.authority.reviewer.id ?? opts['--by']!, at: review?.act.at ?? new Date(now).toISOString(), verbatim: '' };
|
|
182
|
+
if (review) entry.source = `review act ${review.act.jobId}/${review.act.actId}`;
|
|
183
|
+
else if (opts['--source'] !== undefined) entry.source = opts['--source'];
|
|
129
184
|
const refusals: Finding[] = [];
|
|
130
185
|
// Validate against the original reached stage before moving it backwards.
|
|
131
186
|
const candidate = { ...project, reopens: [...asArray(project.reopens), entry] };
|
|
@@ -133,12 +188,20 @@ export function reopenCommand(args: string[], now: number): { stdout: string; ex
|
|
|
133
188
|
collectReopens(candidate, refusals);
|
|
134
189
|
const first = refusals[0];
|
|
135
190
|
if (first !== undefined) refuse(first.code, first.message);
|
|
136
|
-
entry.verbatim = readVerbatim(opts['--verbatim-file'] ?? '-');
|
|
191
|
+
entry.verbatim = review ? review.act.value.words : readVerbatim(opts['--verbatim-file'] ?? '-');
|
|
137
192
|
for (const step of affected) step.status = 'pending';
|
|
138
193
|
project.reopens = candidate.reopens;
|
|
139
194
|
project.stage = entry.stage;
|
|
195
|
+
if (review) {
|
|
196
|
+
project.review_acts = [...asArray(project.review_acts), { schema: 'rig-create/review-act@1', kind: 'reopen',
|
|
197
|
+
jobId: review.act.jobId, actId: review.act.actId, reopen: asArray(project.reopens).length - 1,
|
|
198
|
+
bytes: review.bytes, sha256: jobHash(review.bytes), version_hash: jobHash(JSON.stringify(entry)) }];
|
|
199
|
+
const invalid = reviewActFindings(project)[0];
|
|
200
|
+
if (invalid) refuse(invalid.code, invalid.message);
|
|
201
|
+
}
|
|
140
202
|
const rejected = newRefusals(dir, before, project, { now })[0];
|
|
141
203
|
if (rejected !== undefined) refuse(rejected.code, rejected.message);
|
|
204
|
+
if (review && !readFileSync(file).equals(original)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.reopens: record changed during fetch; retry on the current record');
|
|
142
205
|
writeFileSync(file, `${JSON.stringify(project, null, 2)}\n`);
|
|
143
|
-
return
|
|
206
|
+
return result(entry);
|
|
144
207
|
}
|