@rig-c/create 1.0.1 → 1.0.2
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 +31 -23
- package/docs/CHECKS.md +9 -8
- package/docs/INTERVIEW.md +11 -4
- package/docs/LIVE-REVIEW.md +525 -0
- package/package.json +3 -2
- package/src/agent-guide.ts +16 -13
- package/src/choice-writers.ts +8 -2
- package/src/choices.ts +1 -1
- package/src/cli.ts +9 -1
- package/src/direction-writers.ts +3 -1
- package/src/direction.ts +2 -1
- package/src/feedback.ts +5 -2
- package/src/init.ts +1 -1
- package/src/job-acts.ts +44 -3
- package/src/job-host.ts +1 -1
- package/src/next.ts +113 -53
- package/src/record-input.ts +1 -1
- package/src/reopen.ts +1 -1
- package/src/review-package.ts +2 -1
- package/src/scene-writers.ts +2 -1
- package/src/scene.ts +3 -1
- package/src/signoff-pair.ts +60 -0
- package/src/signoff-target.ts +7 -4
- package/src/signoff.ts +5 -0
- package/src/structure.ts +1 -1
- package/templates/agent-guide.md +29 -31
- package/templates/example-chain/AGENTS.md +41 -43
- package/templates/example-chain/chain.json +55 -55
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +41 -43
- package/templates/example-chain/checkpoints/0a-interview-options.json +1 -1
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +41 -43
- package/templates/example-chain/checkpoints/0b-plan-pending.json +1 -1
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +41 -43
- package/templates/example-chain/checkpoints/1-parts-verdict/project.json +2 -2
- package/templates/example-chain/checkpoints/1-parts-verdict.json +5 -5
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +41 -43
- package/templates/example-chain/checkpoints/2-rigging-answers/project.json +2 -2
- package/templates/example-chain/checkpoints/2-rigging-answers.json +5 -5
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +41 -43
- package/templates/example-chain/checkpoints/3-rig-review/project.json +4 -4
- package/templates/example-chain/checkpoints/3-rig-review.json +9 -9
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +41 -43
- package/templates/example-chain/checkpoints/4-direction-post/project.json +5 -5
- package/templates/example-chain/checkpoints/4-direction-post.json +9 -9
- 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/scene/s1.json +1 -1
- package/templates/example-chain/project.json +10 -10
- 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/{4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json → 52673b635b9cbc92da1a9e3d4c023a249bbacf79cd21b59724f03f18f8caf09b.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 +68 -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/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 +164 -164
- package/templates/example-project.json +249 -249
- package/templates/example-remesh/AGENTS.md +41 -43
- 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/parts-review.json +1 -1
- package/templates/example-remesh/evidence/review-reads/{6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json → afe2522be4087f7f2fa7e97746d5f8574b0aedb474245ce2dbce0455e662f041.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/project.json +225 -225
- package/templates/example-reopened/project.json +289 -289
- package/templates/example-split/captions.json +1 -1
package/src/choices.ts
CHANGED
|
@@ -177,7 +177,7 @@ export function optionContext(project: JsonObject, stage: string, at: number, di
|
|
|
177
177
|
if (!signoff || !rig || !isObject(provenance.config)) fail('INTERVIEW_OPTIONS_STALE', 'accept a rig with retained config before drafting post options');
|
|
178
178
|
const approval = latestPartsApproval(project, date(rig.at));
|
|
179
179
|
const draft = objects(project.drafts).find(d => d.id === approval?.draft_id);
|
|
180
|
-
if (!draft || !isObject(draft.parts)) fail('INTERVIEW_OPTIONS_STALE',
|
|
180
|
+
if (!draft || !isObject(draft.parts)) fail('INTERVIEW_OPTIONS_STALE', `versions[v${String(rig.n)}]: no retained approved draft parts are available at rig.at ${String(rig.at)}; restore the parts-review approval and draft live at that build date, or register a new rig version after parts-review before drafting post options`);
|
|
181
181
|
return { kind: 'rig', rig_version: rig.n as Json, signoff_at: signoff.record.at as Json, config: provenance.config, parts: draft.parts };
|
|
182
182
|
}
|
|
183
183
|
|
package/src/cli.ts
CHANGED
|
@@ -113,12 +113,20 @@ ${INPUT_CLI} rig-create init <dir> [--no-install] create a project
|
|
|
113
113
|
rig-create --help show this help
|
|
114
114
|
rig-create --version show the version
|
|
115
115
|
|
|
116
|
+
job open / job reconcile / bound writers:
|
|
117
|
+
--rig <n> selects a registered rig OR post version; the flag name is shared
|
|
118
|
+
Keep the Review host up across bound writes (including advance and republish).
|
|
119
|
+
If it stopped, job open again with --reviewer <id> --role owner|delegate,
|
|
120
|
+
then job reconcile; a pending publication blocks later bound writes.
|
|
121
|
+
|
|
116
122
|
init:
|
|
117
123
|
<dir> an empty or missing folder; a non-empty one is refused
|
|
118
124
|
--no-install do not run bun install (offline, tests); run it yourself afterwards
|
|
119
125
|
--painting <file> --origin <words>
|
|
120
126
|
copy the painting into inputs/, register it and declare project.painting; refused (INIT_ARTWORK_EXISTS) when a
|
|
121
|
-
project directly under --projects-root (default: the parent folder) binds the same sha256
|
|
127
|
+
project directly under --projects-root (default: the parent folder) binds the same sha256.
|
|
128
|
+
Continue there as a new version; restart keeps its correction ledger.
|
|
129
|
+
A new project is for different artwork or a different rig kind only.
|
|
122
130
|
--same-artwork-reason <file>
|
|
123
131
|
record why this artwork needs a second project (a different rig kind)
|
|
124
132
|
|
package/src/direction-writers.ts
CHANGED
|
@@ -18,6 +18,8 @@ direction write <N> [<dir>] --file <retained JSON> --rig <R> [--answers <version
|
|
|
18
18
|
direction patch <N> [<dir>] --patch-file <retained direction-patch/1> [--answers <version>:<verdict>]
|
|
19
19
|
direction check <N> [<dir>] [--json]
|
|
20
20
|
Documents are immutable evidence/direction/v<N>.json. No rendering or producer builds.
|
|
21
|
+
--file and --patch-file are retained, non-ignored project-relative paths.
|
|
22
|
+
A bounced predecessor needs --answers <version>:<verdict> on write/patch.
|
|
21
23
|
Capture a made correction with correction record --direction-patch-file before patch ingestion;
|
|
22
24
|
resolve it against the result post. Review exports patches, never invokes this writer.`;
|
|
23
25
|
function usage(why:string):never {refuse('DIRECTION_USAGE',`direction: ${why}; ${DIRECTION_HELP}`,USAGE_EXIT);}
|
|
@@ -32,7 +34,7 @@ function prospective(dir:string,before:JsonObject,next:JsonObject,now:number):vo
|
|
|
32
34
|
/** A narrow seam for fault-injected transaction tests; CLI never supplies a hook. */
|
|
33
35
|
export interface DirectionWriteHooks { beforeCommit?:()=>void; }
|
|
34
36
|
export function directionCommand(args:string[],now:number,hooks:DirectionWriteHooks={}):{stdout:string;exit:number} {
|
|
35
|
-
try{return command(args,now,hooks);}catch(e){if(e instanceof DirectionError)refuse(e.code,e.message,e.code==='DIRECTION_USAGE'?USAGE_EXIT:1);throw e;}
|
|
37
|
+
try{return command(args,now,hooks);}catch(e){if(e instanceof DirectionError)refuse(e.code,e.code==='FEEDBACK_UNANSWERED'?`${e.message}; retry direction write/patch with --answers <version>:<verdict> naming the bounced verdict`:e.message,e.code==='DIRECTION_USAGE'?USAGE_EXIT:1);throw e;}
|
|
36
38
|
}
|
|
37
39
|
function command(args:string[],now:number,hooks:DirectionWriteHooks):{stdout:string;exit:number} {
|
|
38
40
|
const action=args[0];const positional:string[]=[];const opts:Record<string,string>={};const answerValues:string[]=[];let json=false;
|
package/src/direction.ts
CHANGED
|
@@ -35,7 +35,8 @@ export function directionFinding(e: DirectionError): Finding { return { code: e.
|
|
|
35
35
|
export function directionBytes(dir: string, path: string): Buffer { return retainedBytes(dir, path, false); }
|
|
36
36
|
function retainedBytes(dir: string, path: string, boundOutput: boolean): Buffer {
|
|
37
37
|
try {
|
|
38
|
-
if (isAbsolute(path) || path.includes('\\') || path.split('/').some(p => !p || p === '.' || p === '..')
|
|
38
|
+
if (isAbsolute(path) || path.includes('\\') || path.split('/').some(p => !p || p === '.' || p === '..')) throw Error('literal project-relative POSIX path required');
|
|
39
|
+
if (!boundOutput && structurePathIsIgnored(dir, path) && !/^derived\/review\/v[1-9][0-9]*\/out\/check\/build\/skeleton\.model\.json$/.test(path)) throw Error('path is ignored; retain the source in a non-ignored project folder such as evidence/direction-src/ and pass that project-relative path');
|
|
39
40
|
let current = realpathSync(dir);
|
|
40
41
|
for (const [i, segment] of path.split('/').entries()) {
|
|
41
42
|
current = join(current, segment); const stat = lstatSync(current);
|
package/src/feedback.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { acquireBoundJobWrite, jobRuntimeExecArgv } from './job-writer.ts';
|
|
|
27
27
|
import { jobHash } from './job-outbox.ts';
|
|
28
28
|
import { assertReviewActSource, decodeReviewAct, reviewActFindings, reviewActPublicationWarning, reviewActStill, reviewActTransport, reviewActVerbatim, reviewActVersionHash, type FetchedReviewAct } from './review-act.ts';
|
|
29
29
|
import { newRefusals } from './refusal-baseline.ts';
|
|
30
|
+
import { ingestedVerdictPairFinding } from './signoff-pair.ts';
|
|
30
31
|
import { dirname, join, resolve } from 'node:path';
|
|
31
32
|
|
|
32
33
|
import { asArray, asString, collectItems, isObject, itemFor, loadCollection, type Ctx, type Item } from './check.ts';
|
|
@@ -541,7 +542,7 @@ function writeVerdict(projectDirArg: string, input: ReviewInput, now: number, fe
|
|
|
541
542
|
bytes: fetched.bytes, sha256: jobHash(fetched.bytes), version: input.version, verdict: verdicts.length - 1,
|
|
542
543
|
version_hash: reviewActVersionHash(original, fetched.act), added_evidence: addedEvidence };
|
|
543
544
|
project.review_acts = [...asArray(project.review_acts), receipt];
|
|
544
|
-
const invalid = reviewActFindings(project)[0] ?? newRefusals(dir, original, project, { now })[0];
|
|
545
|
+
const invalid = reviewActFindings(project)[0] ?? ingestedVerdictPairFinding(project, input.version, verdicts.length - 1) ?? newRefusals(dir, original, project, { now })[0];
|
|
545
546
|
if (invalid) refuse(invalid.code, invalid.message);
|
|
546
547
|
assertReviewActSource(dir, original, fetched.act);
|
|
547
548
|
if (!readFileSync(file).equals(originalBytes)) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json: record changed during verdict validation; retry against the current live job');
|
|
@@ -556,7 +557,7 @@ function writeVerdict(projectDirArg: string, input: ReviewInput, now: number, fe
|
|
|
556
557
|
} finally { closeSync(fd); rmSync(temporary, { force: true }); }
|
|
557
558
|
return result;
|
|
558
559
|
}
|
|
559
|
-
const invalid = newRefusals(dir, original, project, { now })[0];
|
|
560
|
+
const invalid = ingestedVerdictPairFinding(project, input.version, verdicts.length - 1) ?? newRefusals(dir, original, project, { now })[0];
|
|
560
561
|
if (invalid) refuse(invalid.code, invalid.message);
|
|
561
562
|
writeFileSync(file, `${JSON.stringify(project, null, 2)}\n`);
|
|
562
563
|
return { project_dir: dir, verdict_path: verdictPath, verdict, items: out, stage: asString(project.stage) };
|
|
@@ -589,6 +590,8 @@ export function recordReviewAct(root: string, requestedVersion: number, actId: s
|
|
|
589
590
|
? existing.bytes === response.bytes
|
|
590
591
|
: isDeepStrictEqual(JSON.parse(String(existing.bytes)), act);
|
|
591
592
|
if (!sameAct) refuse('REVIEW_ACT_REPLAY_CONFLICT', 'project.json.review_acts: immutable act changed; restore the original receipt');
|
|
593
|
+
const pair = ingestedVerdictPairFinding(project, requestedVersion, Number(existing.verdict));
|
|
594
|
+
if (pair) refuse(pair.code, pair.message);
|
|
592
595
|
if (existing.version_hash !== reviewActVersionHash(project, act, Number(existing.verdict))) refuse('REVIEW_ACT_SOURCE_STALE', 'project.json.review_acts: reviewed version changed; request a new review act');
|
|
593
596
|
const version = asArray(project.versions).find(v => isObject(v) && v.n === requestedVersion) as JsonObject;
|
|
594
597
|
return { project_dir: root, verdict_path: `versions[v${requestedVersion}].verdicts[${existing.verdict}]`,
|
package/src/init.ts
CHANGED
|
@@ -65,7 +65,7 @@ export function sameArtworkFor(dir: string, name: string, sha256: string, now: n
|
|
|
65
65
|
return inputs.some(input => input !== null && typeof input === 'object' && (input as JsonObject).sha256 === sha256);
|
|
66
66
|
});
|
|
67
67
|
if (matches.length > 0 && options.sameArtworkReason === undefined) {
|
|
68
|
-
refuse('INIT_ARTWORK_EXISTS', `inputs[${options.index ?? 0}]: ${name} (sha256 ${sha256}) is already an input of ${matches.join(', ')} under ${root};
|
|
68
|
+
refuse('INIT_ARTWORK_EXISTS', `inputs[${options.index ?? 0}]: ${name} (sha256 ${sha256}) is already an input of ${matches.join(', ')} under ${root}; continue in that existing project as a new version; restart there inherits its correction ledger. A new project is only for different artwork or a different rig kind (--same-artwork-reason <file> for the latter); do not copy the artwork to bypass this refusal`);
|
|
69
69
|
}
|
|
70
70
|
if (options.sameArtworkReason !== undefined) {
|
|
71
71
|
if (matches.length === 0) refuse('INIT_ARTWORK_USAGE', `--same-artwork-reason: no project under ${root} binds ${name}; drop the reason`, USAGE_EXIT);
|
package/src/job-acts.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/** Writer routing for finalized acts. No event transport or automatic ingestion. */
|
|
2
2
|
import type { ActV3 } from '@rig-c/review/job-host';
|
|
3
|
-
import { asArray, isObject } from './check.ts';
|
|
3
|
+
import { asArray, isObject, type Finding } from './check.ts';
|
|
4
|
+
import { existsSync, readFileSync } from './check-reads.ts';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { recordedPartsVerdict } from './parts-verdict-record.ts';
|
|
4
7
|
import { stageOfStep } from './stages.ts';
|
|
8
|
+
import { checkStructure } from './structure.ts';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
5
10
|
import type { JsonObject } from './templates.ts';
|
|
6
11
|
|
|
7
12
|
export interface PendingJobAct {
|
|
@@ -11,6 +16,7 @@ export interface PendingJobAct {
|
|
|
11
16
|
at: string;
|
|
12
17
|
args: string[] | null;
|
|
13
18
|
command: string | null;
|
|
19
|
+
preconditions?: { command: string | null; act: string }[];
|
|
14
20
|
unavailable?: { code: 'JOB_ACTS_WRITER_UNAVAILABLE'; message: string };
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -18,6 +24,23 @@ function shellWord(value: string): string {
|
|
|
18
24
|
return /^[A-Za-z0-9_./:-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
/** A retained snapshot is reusable only for this exact draft and config.
|
|
28
|
+
* Never overwrite unrelated or invalid bytes: choose the next retained name.
|
|
29
|
+
*/
|
|
30
|
+
export function partsStructureSnapshot(root: string, project: JsonObject, draft: string): { path: string; valid: boolean } {
|
|
31
|
+
const stem = `evidence/structure-${encodeURIComponent(draft)}`;
|
|
32
|
+
for (let suffix = 0; ; suffix++) {
|
|
33
|
+
const path = `${stem}${suffix === 0 ? '' : `-${suffix}`}.json`;
|
|
34
|
+
if (!existsSync(join(root, path))) return { path, valid: false };
|
|
35
|
+
try {
|
|
36
|
+
const bytes = readFileSync(join(root, path));
|
|
37
|
+
const errors: Finding[] = [];
|
|
38
|
+
const signoff = { draft_id: draft, structure: { path, sha256: createHash('sha256').update(bytes).digest('hex') } };
|
|
39
|
+
if (checkStructure(root, project, signoff, path, errors) !== null && errors.length === 0) return { path, valid: true };
|
|
40
|
+
} catch { /* Preserve the existing bytes and select another output path. */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
export function pendingJobActs(project: JsonObject, acts: readonly ActV3[], root: string): PendingJobAct[] {
|
|
22
45
|
const ingested = new Set(asArray(project.review_acts).filter(isObject).map(row => JSON.stringify([row.jobId, row.actId])));
|
|
23
46
|
const signoffs = asArray(project.signoffs).filter(isObject);
|
|
@@ -28,6 +51,7 @@ export function pendingJobActs(project: JsonObject, acts: readonly ActV3[], root
|
|
|
28
51
|
seen.add(key);
|
|
29
52
|
let subject: string = act.kind;
|
|
30
53
|
let args: string[] | null = null;
|
|
54
|
+
const preconditions: { command: string | null; act: string }[] = [];
|
|
31
55
|
switch (act.kind) {
|
|
32
56
|
case 'review-verdict': {
|
|
33
57
|
const selector = act.source.selector;
|
|
@@ -51,10 +75,26 @@ export function pendingJobActs(project: JsonObject, acts: readonly ActV3[], root
|
|
|
51
75
|
subject = `question ${act.value.questionId}`;
|
|
52
76
|
args = ['interview-answer', act.value.questionId, root];
|
|
53
77
|
break;
|
|
54
|
-
case 'signoff':
|
|
78
|
+
case 'signoff': {
|
|
55
79
|
subject = `stage ${act.value.stage}`;
|
|
56
80
|
args = ['signoff', act.value.stage, root];
|
|
81
|
+
if (act.value.stage === 'parts-review') {
|
|
82
|
+
const selector = act.source.selector;
|
|
83
|
+
// Select only the draft shown by Review, never the latest draft by guess.
|
|
84
|
+
if (selector?.kind !== 'draft') { args = null; break; }
|
|
85
|
+
const draft = selector.id;
|
|
86
|
+
const structure = partsStructureSnapshot(root, project, draft);
|
|
87
|
+
let approved = false;
|
|
88
|
+
try { recordedPartsVerdict(root, project, draft); approved = true; } catch { /* Ordered prerequisite below. */ }
|
|
89
|
+
if (!approved) preconditions.push({ command: null, act: `ingest the approved parts-verdict for draft ${draft} before structure-snapshot; use its job acts writer command` });
|
|
90
|
+
if (!structure.valid) preconditions.push({
|
|
91
|
+
command: ['rig-create', 'structure-snapshot', root, '--draft', draft, '--out', structure.path].map(shellWord).join(' '),
|
|
92
|
+
act: 'after ingesting the approved parts-verdict, retain the structure snapshot before signoff',
|
|
93
|
+
});
|
|
94
|
+
args.push('--draft', draft, '--structure-file', structure.path);
|
|
95
|
+
}
|
|
57
96
|
break;
|
|
97
|
+
}
|
|
58
98
|
case 'checkpoint': {
|
|
59
99
|
const step = asArray(isObject(project.plan) ? project.plan.steps : undefined).filter(isObject).find(step => step.id === act.value.planStepId);
|
|
60
100
|
subject = `checkpoint ${act.value.planStepId}`;
|
|
@@ -68,6 +108,7 @@ export function pendingJobActs(project: JsonObject, acts: readonly ActV3[], root
|
|
|
68
108
|
}
|
|
69
109
|
if (args) args.push('--from-review', act.actId);
|
|
70
110
|
return [{ actId: act.actId, kind: act.kind, subject, at: act.at, args,
|
|
111
|
+
...(preconditions.length ? { preconditions } : {}),
|
|
71
112
|
command: args ? ['rig-create', ...args].map(shellWord).join(' ') : null,
|
|
72
113
|
...(args ? {} : { unavailable: { code: 'JOB_ACTS_WRITER_UNAVAILABLE' as const,
|
|
73
114
|
message: `project.json: act ${act.actId} (${subject}) has no current writer target; restore its plan step or use a supported Review subject before ingestion` } }) }];
|
|
@@ -76,5 +117,5 @@ export function pendingJobActs(project: JsonObject, acts: readonly ActV3[], root
|
|
|
76
117
|
|
|
77
118
|
export function jobActsText(result: { acts: readonly PendingJobAct[] }): string {
|
|
78
119
|
if (!result.acts.length) return 'job: no finalized acts awaiting ingestion';
|
|
79
|
-
return result.acts.map(act => `${act.actId} ${act.kind} ${act.subject} at ${act.at}\n ${act.command ?? `${act.unavailable!.code}: ${act.unavailable!.message}`}`).join('\n');
|
|
120
|
+
return result.acts.map(act => `${act.actId} ${act.kind} ${act.subject} at ${act.at}\n${(act.preconditions ?? []).map(step => ` first: ${step.command ?? step.act}\n`).join('')} ${act.command ?? `${act.unavailable!.code}: ${act.unavailable!.message}`}`).join('\n');
|
|
80
121
|
}
|
package/src/job-host.ts
CHANGED
|
@@ -424,7 +424,7 @@ export function requestedRecordResult(root: string, question?: string, plan = fa
|
|
|
424
424
|
if (question !== undefined) {
|
|
425
425
|
const items = collectItems(project, { collection: loadCollection(root, project) }).filter(item => item.id === question);
|
|
426
426
|
const binding = items.length === 1 ? items[0]!.record.pending_options : undefined;
|
|
427
|
-
if (!isObject(binding) || typeof binding.path !== 'string') refuse('JOB_RECORD_SUBJECT_INVALID',
|
|
427
|
+
if (!isObject(binding) || typeof binding.path !== 'string') refuse('JOB_RECORD_SUBJECT_INVALID', `project.json: requested question has no unique pending option set; register it with rig-create interview-options ${question} '${root.replaceAll("'", "'\\''")}' --file '<option-set.json>' first (draft a new option set to re-ask an answered question)`);
|
|
428
428
|
const set = JSON.parse(publicationFile(root, binding.path).toString('utf8')) as JsonObject;
|
|
429
429
|
if (typeof set.id !== 'string') refuse('JOB_RECORD_SUBJECT_INVALID', 'project.json: pending option set has no id; restore its original identity');
|
|
430
430
|
result = { kind: 'option-set', questionId: question, setId: set.id };
|
package/src/next.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveDiffMapPartsFile,
|
|
25
25
|
validateDiffMapRecord,
|
|
26
26
|
} from './diff-map.ts';
|
|
27
|
+
import { partsStructureSnapshot } from './job-acts.ts';
|
|
27
28
|
import { JOB_BINDING_FILE, type WriterBinding } from './job-outbox.ts';
|
|
28
29
|
import { isPartsCheckMove, partsCheckRecord } from './parts-check.ts';
|
|
29
30
|
import { recordedPartsVerdict } from './parts-verdict-record.ts';
|
|
@@ -50,9 +51,9 @@ import {
|
|
|
50
51
|
STAGE_VALUES,
|
|
51
52
|
stageIndex,
|
|
52
53
|
stageOfStep,
|
|
53
|
-
stepCoveredByApproval,
|
|
54
54
|
} from './stages.ts';
|
|
55
55
|
import type { Json, JsonObject } from './templates.ts';
|
|
56
|
+
import { sceneCurrentAt } from './scene.ts';
|
|
56
57
|
|
|
57
58
|
export const NEXT_SCHEMA = 'rig-create/next@1';
|
|
58
59
|
|
|
@@ -109,6 +110,8 @@ export interface NextReport {
|
|
|
109
110
|
accepted: boolean;
|
|
110
111
|
refusals: NextRefusal[];
|
|
111
112
|
warnings?: Finding[];
|
|
113
|
+
/** A view of existing answers, never a new project record. */
|
|
114
|
+
answers?: { question_id: string; field: string; by: Json; at: Json; picks: { ask: Json; option_id: Json; label: Json }[]; command?: string }[];
|
|
112
115
|
awaits: string[];
|
|
113
116
|
moves: NextMove[];
|
|
114
117
|
}
|
|
@@ -191,6 +194,14 @@ function isOpenInterviewTodo(finding: Finding, effectiveStage: string): boolean
|
|
|
191
194
|
return isOpenInterviewStage(effectiveStage) && finding.code === 'INTERVIEW_INCOMPLETE';
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
function shellWord(value: string): string {
|
|
198
|
+
return /^[A-Za-z0-9_./:-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function optionCommand(id: string, dir: string): string {
|
|
202
|
+
return `rig-create interview-options ${shellWord(id)} ${shellWord(dir)} --file '<option-set.json>'`;
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
function candidateAdvance(
|
|
195
206
|
dir: string,
|
|
196
207
|
project: JsonObject,
|
|
@@ -264,8 +275,8 @@ function candidateAdvance(
|
|
|
264
275
|
const checked = checkProject(dir, clone, { now, forwardWrite: true, readOnly: true });
|
|
265
276
|
if (newFindings(allowed, checked.refusals).length > 0) return null;
|
|
266
277
|
return partsCheck
|
|
267
|
-
? `rig-create advance ${targetStage} --parts-check ${partsCheck.report} --parts ${partsCheck.root}`
|
|
268
|
-
: `rig-create advance ${targetStage}`;
|
|
278
|
+
? `rig-create advance ${targetStage} ${shellWord(dir)} --parts-check ${shellWord(partsCheck.report)} --parts ${shellWord(partsCheck.root)}`
|
|
279
|
+
: `rig-create advance ${targetStage} ${shellWord(dir)}`;
|
|
269
280
|
}
|
|
270
281
|
|
|
271
282
|
function candidateStepStatus(
|
|
@@ -327,7 +338,7 @@ function candidateStepStatus(
|
|
|
327
338
|
const allowed = carriedRefusals(dir, before, { now, forwardWrite: false });
|
|
328
339
|
const checked = checkProject(dir, clone, { now, forwardWrite: false, readOnly: true });
|
|
329
340
|
if (newFindings(allowed, checked.refusals).length > 0) return null;
|
|
330
|
-
return `rig-create step-status ${stepId} ${nextStatus} --by agent`;
|
|
341
|
+
return `rig-create step-status ${shellWord(stepId)} ${nextStatus} ${shellWord(dir)} --by agent`;
|
|
331
342
|
}
|
|
332
343
|
|
|
333
344
|
function nextStageInOrder(project: JsonObject, currentStage: string): string | null {
|
|
@@ -362,6 +373,7 @@ function resolveLaggingAdvance(
|
|
|
362
373
|
}
|
|
363
374
|
return null;
|
|
364
375
|
}
|
|
376
|
+
if (stage === 'post' && (!asArray(project.scenes).length || !asArray(project.versions).some(v => isObject(v) && v.kind === 'post'))) return null;
|
|
365
377
|
const cmd = candidateAdvance(dir, project, nextStage, now);
|
|
366
378
|
return cmd !== null ? { targetStage: nextStage, command: cmd } : null;
|
|
367
379
|
}
|
|
@@ -473,7 +485,7 @@ function resolveDiffComponentsMove(
|
|
|
473
485
|
const recordedPaintingSha = (draftReport.painting as { sha256: string }).sha256;
|
|
474
486
|
if (stackSha === recordedStackSha && paintingSha === recordedPaintingSha) {
|
|
475
487
|
return {
|
|
476
|
-
command: `cp ${draftReportPath} evidence/diff-components.json`,
|
|
488
|
+
command: `cp ${shellWord(resolve(dir, draftReportPath))} ${shellWord(resolve(dir, 'evidence/diff-components.json'))}`,
|
|
477
489
|
act: `copy ${draftReportPath} for drafts[${draftId}] to evidence/diff-components.json`,
|
|
478
490
|
};
|
|
479
491
|
}
|
|
@@ -483,9 +495,9 @@ function resolveDiffComponentsMove(
|
|
|
483
495
|
}
|
|
484
496
|
}
|
|
485
497
|
|
|
486
|
-
const figureFlag = figureMaskPath !== undefined ? ` --figure ${figureMaskPath}` : '';
|
|
498
|
+
const figureFlag = figureMaskPath !== undefined ? ` --figure ${shellWord(resolve(dir, figureMaskPath))}` : '';
|
|
487
499
|
return {
|
|
488
|
-
command: `rig-parts diff-components --stack ${stackPath} --painting ${paintingPath} --parts ${partsRoot
|
|
500
|
+
command: `rig-parts diff-components --stack ${shellWord(resolve(dir, stackPath))} --painting ${shellWord(resolve(dir, paintingPath))} --parts ${shellWord(resolve(dir, partsRoot, 'parts.json'))}${figureFlag} --floor 2 --threshold 40 --out ${shellWord(resolve(dir, 'evidence/diff-components.json'))}`,
|
|
489
501
|
act: `run rig-parts diff-components for drafts[${draftId}]`,
|
|
490
502
|
};
|
|
491
503
|
}
|
|
@@ -524,13 +536,13 @@ function resolveDiffClassesCommand(dir: string): string {
|
|
|
524
536
|
const bytes = diffReportBytes(dir, 'evidence/diff-components.json');
|
|
525
537
|
const parsed = JSON.parse(bytes.toString('utf8')) as Json;
|
|
526
538
|
if (isObject(parsed) && reportShape(parsed) && (parsed.components as unknown[]).length === 0) {
|
|
527
|
-
return
|
|
539
|
+
return `rig-create diff-classes ${shellWord(dir)} --diff-report evidence/diff-components.json --by agent`;
|
|
528
540
|
}
|
|
529
541
|
} catch {
|
|
530
542
|
// Fall through to default command with --triage-file.
|
|
531
543
|
}
|
|
532
544
|
}
|
|
533
|
-
return
|
|
545
|
+
return `rig-create diff-classes ${shellWord(dir)} --diff-report evidence/diff-components.json --triage-file evidence/diff-triage.json --by agent`;
|
|
534
546
|
}
|
|
535
547
|
|
|
536
548
|
export function nextProject(projectDirArg: string, now: number = Date.now()): NextReport {
|
|
@@ -543,7 +555,18 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
543
555
|
const stage = String(project.stage ?? '');
|
|
544
556
|
const effectiveStage = String(report.effective_stage ?? stage);
|
|
545
557
|
const structureWarnings = report.warnings.filter(w => w.code === 'STRUCTURE_DELTA_REVIEW');
|
|
546
|
-
const
|
|
558
|
+
const answers = collectItems(project, { collection: loadCollection(dir) }).flatMap(item => {
|
|
559
|
+
const answer = item.state.state === 'user' || item.state.state === 'delegated' || item.state.state === 'not_applicable'
|
|
560
|
+
? item.state.answer : isObject(item.raw) ? item.raw : null;
|
|
561
|
+
if (!answer) return [];
|
|
562
|
+
return [{ question_id: item.id, field: item.path, by: answer.by ?? null, at: answer.at ?? null,
|
|
563
|
+
picks: asArray(isObject(answer.choice) ? answer.choice.picks : undefined).filter(isObject).map(pick => ({
|
|
564
|
+
ask: pick.ask ?? null, option_id: pick.option_id ?? null, label: isObject(pick.shown) ? pick.shown.label ?? null : null,
|
|
565
|
+
})),
|
|
566
|
+
...(interviewStageOfGuide(item.id, item.kind === 'post', hasStageOrder(project)) === effectiveStage
|
|
567
|
+
? { command: optionCommand(item.id, dir) } : {}) }];
|
|
568
|
+
});
|
|
569
|
+
const warningFields = { ...(structureWarnings.length ? { warnings: structureWarnings } : {}), answers };
|
|
547
570
|
const jobId = readOptionalJobId(dir);
|
|
548
571
|
const holes = report.holes ?? [];
|
|
549
572
|
|
|
@@ -559,7 +582,7 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
559
582
|
? resolveLaggingAdvance(dir, project, stage, now)
|
|
560
583
|
: null;
|
|
561
584
|
const isResolvableLagging = (finding: Finding): boolean =>
|
|
562
|
-
finding.code === 'STAGE_LAGGING' && laggingAdvance !== null;
|
|
585
|
+
finding.code === 'STAGE_LAGGING' && (laggingAdvance !== null || stage === 'post' && (!asArray(project.scenes).length || !asArray(project.versions).some(v => isObject(v) && v.kind === 'post')));
|
|
563
586
|
|
|
564
587
|
const hasBlockingRefusal =
|
|
565
588
|
holes.length > 0 ||
|
|
@@ -597,14 +620,32 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
597
620
|
groups.set(kind, [...(groups.get(kind) ?? []), { version: summary.version, verdict: summary.index }]);
|
|
598
621
|
}
|
|
599
622
|
for (const [kind, refs] of groups) {
|
|
623
|
+
if (kind === 'post') {
|
|
624
|
+
// Match the direction writer's authoring gate, including earlier
|
|
625
|
+
// accepted verdicts that a later bounce does not itself reopen.
|
|
626
|
+
const reopens = collectReopens(project, []);
|
|
627
|
+
const closed = collectSignoffs(project, []).some(s => s.stage === 'direction-review'
|
|
628
|
+
&& s.checkpoint === null && liveSignoff(reopens, s.stage, s.at))
|
|
629
|
+
|| versions.some(v => v.kind === 'post' && asArray(v.verdicts).filter(isObject).some(d =>
|
|
630
|
+
d.review === 'direction-review' && d.verdict === 'accepted'
|
|
631
|
+
&& liveSignoff(reopens, 'direction-review', Date.parse(String(d.at)))));
|
|
632
|
+
if (closed) {
|
|
633
|
+
const target = 'reopens[stage=direction-review]';
|
|
634
|
+
addAwait(target);
|
|
635
|
+
moves.push({ role: 'human', field: 'reopens', awaits: target,
|
|
636
|
+
command: `rig-create reopen direction-review ${shellWord(dir)} --by '<human>' --verbatim-file '<absolute-human-words-file>' --source '<reopen-act-id>'`,
|
|
637
|
+
act: 'Until create#493, an agent may type this human reopen only from Review host raw export act@4: save its unchanged nonempty value.words byte-for-byte to an absolute verbatim-file path and name actId with --source. Empty/missing words: STOP; the agent never supplies words. Do not use job fetch-act (it rejects act@4). Create cannot ingest Review reopen acts yet.' });
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
600
641
|
const field = `versions[n=${n}]`;
|
|
601
642
|
addAwait(field);
|
|
602
643
|
const writer = kind === 'rig' ? `rig-version ${n}` : `direction write ${n}`;
|
|
603
644
|
const inputs = kind === 'rig'
|
|
604
|
-
? '--parts <dir> --rig <dir> --build <dir> --idle <dir> --config <file> --path-adapter <file> --compile-report <file> --toolchain-lock <file> --at-source <file> --at-pointer <pointer> --by <agent>'
|
|
645
|
+
? '--parts <dir> --rig <dir> --build <dir> --idle <dir> --config <file> --path-adapter <file> --compile-report <file> --toolchain-lock <file> --run-record <run.json> --at-source <file> --at-pointer <pointer> --by <agent>'
|
|
605
646
|
: '--file <direction.json> --rig <rig-version>';
|
|
606
647
|
moves.push({ role: 'agent', field: 'versions', awaits: field,
|
|
607
|
-
command: `rig-create ${writer} ${inputs} ${refs.map(r => `--answers ${r.version}:${r.verdict}`).join(' ')}`,
|
|
648
|
+
command: `rig-create ${writer} ${shellWord(dir)} ${inputs.replace(/<[^>]+>/g, shellWord)} ${refs.map(r => `--answers ${r.version}:${r.verdict}`).join(' ')}`,
|
|
608
649
|
act: `build the next ${kind} version with ${writer}, recording answers_feedback ${JSON.stringify(refs)}` });
|
|
609
650
|
}
|
|
610
651
|
if (moves.length > 0) return { schema: NEXT_SCHEMA, project_id: String(project.name ?? ''), stage, effective_stage: effectiveStage, accepted: false, refusals, ...warningFields, awaits: awaitsList, moves };
|
|
@@ -816,17 +857,11 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
816
857
|
});
|
|
817
858
|
} else {
|
|
818
859
|
moves.push({
|
|
819
|
-
role: '
|
|
860
|
+
role: 'agent',
|
|
820
861
|
field: answerField,
|
|
821
862
|
awaits: answerField,
|
|
822
|
-
command:
|
|
823
|
-
act: `
|
|
824
|
-
review_act: {
|
|
825
|
-
kind: 'interview-picks',
|
|
826
|
-
stage: effectiveStage,
|
|
827
|
-
question_id: item.id,
|
|
828
|
-
...(jobId ? { job_id: jobId } : {}),
|
|
829
|
-
},
|
|
863
|
+
command: optionCommand(item.id, dir),
|
|
864
|
+
act: `draft an option-set file for ${item.id}, then register it for a Review pick`,
|
|
830
865
|
unanswered_by_record: `${questionPath}.pending_options`,
|
|
831
866
|
});
|
|
832
867
|
}
|
|
@@ -844,25 +879,26 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
844
879
|
}
|
|
845
880
|
}
|
|
846
881
|
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
882
|
+
const planIds = effectiveStage === 'rigging-interview' ? ['bones', 'meshes', 'constraints', 'motion']
|
|
883
|
+
: effectiveStage === 'post-interview' ? ['camera', 'timing'] : ['seethrough', 'assemble'];
|
|
884
|
+
const missingPlanIds = planIds.filter(id => {
|
|
885
|
+
const guide = id === 'camera' ? 'camera_depth' : id === 'timing' ? 'post_timing' : id;
|
|
886
|
+
const phase = effectiveStage === 'post-interview' ? 'post' : 'rig';
|
|
887
|
+
return !steps.some(step => step.phase === phase && (step.id === id
|
|
888
|
+
|| phase === 'post' && asArray(step.rests_on).includes(guide)));
|
|
889
|
+
});
|
|
890
|
+
const needsPlanSteps = missingPlanIds.length > 0;
|
|
851
891
|
if (needsPlanSteps) {
|
|
852
892
|
addAwait('plan.steps');
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
act: `author the ${id} plan step for approval at rigging-interview; supply order, does, produces, rests-on, bar and checkpoint-proposed`,
|
|
863
|
-
unanswered_by_record: 'plan.steps',
|
|
864
|
-
});
|
|
865
|
-
}
|
|
893
|
+
for (const [index, id] of missingPlanIds.entries()) {
|
|
894
|
+
const guideId = id === 'camera' ? 'camera_depth' : id === 'timing' ? 'post_timing' : id;
|
|
895
|
+
const item = stageItems.find(item => item.id === guideId);
|
|
896
|
+
if (!item || item.state.state === 'silent' || (item.state.state === 'delegated' && !item.state.decided)) continue;
|
|
897
|
+
const order = Math.max(0, ...steps.map(step => Number(step.order) || 0)) + index + 1;
|
|
898
|
+
moves.push({ role: 'agent', field: 'plan.steps', awaits: 'plan.steps',
|
|
899
|
+
command: `rig-create plan add ${id} ${shellWord(dir)} --by agent --phase ${effectiveStage === 'post-interview' ? 'post' : 'rig'} --order ${order} --does '<work>' --produces '<result>' --rests-on ${guideId} --bar '<acceptance bar>' --checkpoint-proposed false`,
|
|
900
|
+
act: `author the ${id} plan step for approval at ${effectiveStage}; replace the work, result and acceptance bar placeholders`,
|
|
901
|
+
unanswered_by_record: 'plan.steps' });
|
|
866
902
|
}
|
|
867
903
|
}
|
|
868
904
|
|
|
@@ -877,7 +913,7 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
877
913
|
act: needsPlanSteps
|
|
878
914
|
? effectiveStage === 'rigging-interview'
|
|
879
915
|
? 'review-tool act signoff for stage rigging-interview (unanswered by record: plan.steps has no rig steps approved at rigging-interview)'
|
|
880
|
-
: `review-tool act signoff for stage ${effectiveStage} (unanswered by record: plan.steps is empty in project.json)`
|
|
916
|
+
: `review-tool act signoff for stage ${effectiveStage} (unanswered by record: ${effectiveStage === 'post-interview' ? 'plan.steps has no post steps' : steps.length === 0 ? 'plan.steps is empty' : 'plan.steps has uncovered parts guides'} in project.json)`
|
|
881
917
|
: effectiveStage === 'interview'
|
|
882
918
|
? 'review-tool act signoff for stage interview on the record page with job open --plan (source-linked Review); ingest signoff interview --from-review <actId>, then advance plan-approved'
|
|
883
919
|
: `review-tool act signoff for stage ${effectiveStage}`,
|
|
@@ -992,6 +1028,14 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
992
1028
|
: undefined;
|
|
993
1029
|
const hasApprovedPartsVerdict = hasApprovedPartsVerdictReceipt(dir, project, draftId);
|
|
994
1030
|
|
|
1031
|
+
if (effectiveStage === 'parts' && partsRoot !== undefined
|
|
1032
|
+
&& candidateAdvance(dir, project, 'parts-review', now, { report: 'evidence/parts-check.json', root: partsRoot }) === null) {
|
|
1033
|
+
addAwait('evidence/parts-check.json');
|
|
1034
|
+
moves.push({ role: 'producer', field: 'drafts', awaits: 'evidence/parts-check.json',
|
|
1035
|
+
command: `rig-parts check-parts --parts ${shellWord(resolve(dir, partsRoot))} --out ${shellWord(resolve(dir, 'evidence'))}`,
|
|
1036
|
+
act: 'produce and retain evidence/parts-check.json before advance parts-review' });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
995
1039
|
if (effectiveStage === 'parts' && partsRoot !== undefined && existsSync(join(dir, 'evidence/parts-check.json'))) {
|
|
996
1040
|
const advCmd = candidateAdvance(dir, project, 'parts-review', now, {
|
|
997
1041
|
report: 'evidence/parts-check.json',
|
|
@@ -1026,17 +1070,15 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
1026
1070
|
});
|
|
1027
1071
|
}
|
|
1028
1072
|
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (draftId !== undefined && hasApprovedPartsVerdict && !hasStructureFile) {
|
|
1033
|
-
addAwait('evidence/structure.json');
|
|
1073
|
+
const structure = draftId === undefined ? undefined : partsStructureSnapshot(dir, project, draftId);
|
|
1074
|
+
if (draftId !== undefined && hasApprovedPartsVerdict && structure !== undefined && !structure.valid) {
|
|
1075
|
+
addAwait(structure.path);
|
|
1034
1076
|
moves.push({
|
|
1035
1077
|
role: 'agent',
|
|
1036
1078
|
field: `drafts[${draftId}]`,
|
|
1037
|
-
awaits:
|
|
1038
|
-
command: `rig-create structure-snapshot --draft ${draftId} --out
|
|
1039
|
-
act: `write structure-snapshot for drafts[${draftId}] to
|
|
1079
|
+
awaits: structure.path,
|
|
1080
|
+
command: `rig-create structure-snapshot ${shellWord(dir)} --draft ${shellWord(draftId)} --out ${shellWord(structure.path)}`,
|
|
1081
|
+
act: `write structure-snapshot for drafts[${draftId}] to ${structure.path}`,
|
|
1040
1082
|
});
|
|
1041
1083
|
}
|
|
1042
1084
|
|
|
@@ -1085,7 +1127,7 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
1085
1127
|
awaits: signoffAwait,
|
|
1086
1128
|
command: null,
|
|
1087
1129
|
act: hasApprovedPartsVerdict
|
|
1088
|
-
?
|
|
1130
|
+
? `review-tool act signoff for stage parts-review; after structure-snapshot, ingest rig-create signoff parts-review ${shellWord(dir)} --draft ${shellWord(draftId!)} --structure-file ${shellWord(structure!.path)} --from-review '<actId>'`
|
|
1089
1131
|
: `review-tool act signoff for stage parts-review (unanswered by record: review_acts[kind=parts-verdict,draft_id=${draftId ?? 'unknown'}] is absent)`,
|
|
1090
1132
|
review_act: {
|
|
1091
1133
|
kind: 'signoff',
|
|
@@ -1095,7 +1137,7 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
1095
1137
|
},
|
|
1096
1138
|
...(!hasApprovedPartsVerdict
|
|
1097
1139
|
? {
|
|
1098
|
-
unanswered_by_record: `review_acts[kind=parts-verdict,draft_id=${draftId ?? 'unknown'}],
|
|
1140
|
+
unanswered_by_record: `review_acts[kind=parts-verdict,draft_id=${draftId ?? 'unknown'}], ${structure?.path ?? 'structure snapshot'}`,
|
|
1099
1141
|
}
|
|
1100
1142
|
: {}),
|
|
1101
1143
|
});
|
|
@@ -1323,7 +1365,7 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
1323
1365
|
);
|
|
1324
1366
|
const nextPostN = postN ?? maxVersionN + 1;
|
|
1325
1367
|
|
|
1326
|
-
if (effectiveStage === 'post') {
|
|
1368
|
+
if (effectiveStage === 'post' && postN !== null && scenes.length > 0) {
|
|
1327
1369
|
const advCmd = candidateAdvance(dir, project, 'direction-review', now);
|
|
1328
1370
|
if (advCmd !== null) {
|
|
1329
1371
|
moves.push({
|
|
@@ -1336,9 +1378,27 @@ export function nextProject(projectDirArg: string, now: number = Date.now()): Ne
|
|
|
1336
1378
|
}
|
|
1337
1379
|
}
|
|
1338
1380
|
|
|
1339
|
-
if (
|
|
1340
|
-
|
|
1381
|
+
if (postN === null) {
|
|
1382
|
+
const rig = versions.filter(v => v.kind === 'rig').at(-1);
|
|
1383
|
+
const rigN = typeof rig?.n === 'number' ? String(rig.n) : '<rig-version>';
|
|
1384
|
+
const currentScene = sceneCurrentAt(dir, project, now);
|
|
1385
|
+
if (!isObject(project.direction_contract)) {
|
|
1386
|
+
addAwait('direction_contract');
|
|
1387
|
+
moves.push({ role: 'agent', field: 'direction_contract', awaits: 'direction_contract',
|
|
1388
|
+
command: `rig-create direction --adopt ${shellWord(dir)}`, act: 'adopt direction before authoring the post' });
|
|
1389
|
+
} else if (currentScene === undefined) {
|
|
1390
|
+
addAwait('scenes[n=1]');
|
|
1391
|
+
moves.push({ role: 'agent', field: 'scenes', awaits: 'scenes[n=1]',
|
|
1392
|
+
command: `rig-create scene write ${shellWord(dir)} --rig ${shellWord(rigN)} --background '<background.png>' --citations-file '<scene-citations.json>'`,
|
|
1393
|
+
act: 'author scene inputs matching the current post_composition Review choice, then write the scene' });
|
|
1394
|
+
} else {
|
|
1395
|
+
addAwait(`versions[n=${nextPostN}]`);
|
|
1396
|
+
moves.push({ role: 'agent', field: 'versions', awaits: `versions[n=${nextPostN}]`,
|
|
1397
|
+
command: `rig-create direction write ${nextPostN} ${shellWord(dir)} --file '<direction.json>' --rig ${shellWord(rigN)}`,
|
|
1398
|
+
act: 'author a direction document binding the current scene, then record the post version' });
|
|
1399
|
+
}
|
|
1341
1400
|
}
|
|
1401
|
+
if (scenes.length === 0) addAwait('scenes[n=1]');
|
|
1342
1402
|
if (postN === null) {
|
|
1343
1403
|
addAwait(`versions[n=${nextPostN}]`);
|
|
1344
1404
|
const verdictAwait = `versions[n=${nextPostN}].verdicts[review=direction-review]`;
|
package/src/record-input.ts
CHANGED
|
@@ -8,7 +8,7 @@ export function readVerbatim(file: string): string {
|
|
|
8
8
|
try {
|
|
9
9
|
bytes = readFileSync(file === '-' ? 0 : file);
|
|
10
10
|
} catch (error) {
|
|
11
|
-
refuse('VERBATIM_UNREADABLE', `${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
11
|
+
refuse('VERBATIM_UNREADABLE', `${file}: ${error instanceof Error ? error.message : String(error)}; --verbatim-file paths resolve against the current working directory, not the project folder; supply an absolute path or run from the folder containing the file`);
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
14
|
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
package/src/reopen.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function liveSignoff(reopens: readonly Reopen[], stage: string, at: numbe
|
|
|
95
95
|
export const REOPEN_HELP = `reopen:
|
|
96
96
|
<stage> [<project dir>] record a human backward move and set project.stage
|
|
97
97
|
--by <who> human signer (agent is refused)
|
|
98
|
-
--verbatim-file <file> unchanged UTF-8 words, or - for stdin (default)
|
|
98
|
+
--verbatim-file <file> unchanged UTF-8 words; relative to the current working directory, not the project; or - for stdin (default)
|
|
99
99
|
--source <text> optional origin of the words, separate from verbatim
|
|
100
100
|
--json print the reopen record
|
|
101
101
|
The clock supplies at; sign-offs and versions remain history.`;
|
package/src/review-package.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { recordedPartsVerdict, recordedPartsFinding } from './parts-verdict-record.ts';
|
|
2
|
+
import reviewPackage from '@rig-c/review/package.json';
|
|
2
3
|
import { configInputReferences, configInputsLegacy, DIRECTORY_INPUT_RECEIPT, plateReceiptSpec, receiptSpec, type ConfigInputsWarning } from './config-inputs.ts';
|
|
3
4
|
import { DirectionError, objects as directionObjects, readDirection } from './direction.ts';
|
|
4
5
|
/** Frozen producer inputs in review's fixed discovery layout (create#38). */
|
|
@@ -40,7 +41,7 @@ function checkBuildIdentity(ctx: Pick<Ctx, 'dir' | 'project' | 'refusals' | 'war
|
|
|
40
41
|
if (finding.code === 'PARTS_VERDICT_FILE_LEGACY') {
|
|
41
42
|
if (!ctx.warnings.some(existing => existing.code === finding.code && existing.path === finding.path)) ctx.warnings.push(finding);
|
|
42
43
|
} else findings.push({ code: 'REVIEW_PACKAGE_PARTS_UNREAD', path: `${path}.files[out/parts.json]`,
|
|
43
|
-
message: `${path}.files[out/parts.json]: accepted parts receipt
|
|
44
|
+
message: `${path}.files[out/parts.json]: accepted parts receipt cannot be read (${finding.code}: ${finding.message}); compare the Review version that recorded the act (not carried by API act receipts) with the current @rig-c/review ${reviewPackage.version} reader. For an act parsing failure, register with a Review version that parses that act, or wait until support lands; preserve the retained bytes. For missing or changed evidence, restore its recorded API response and selected draft before packaging this version` });
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
recordedPartsVerdict(ctx.dir, ctx.project, approval.draft_id, approval.evidence, { requireAvailable: false });
|
package/src/scene-writers.ts
CHANGED
|
@@ -12,7 +12,8 @@ import type { JsonObject } from './templates.ts';
|
|
|
12
12
|
|
|
13
13
|
export const SCENE_HELP = `scene write --rig <R> --background <path> [--foreground <path>]
|
|
14
14
|
[--background-rect x,y,w,h] [--foreground-rect x,y,w,h]
|
|
15
|
-
--citations-file <
|
|
15
|
+
--citations-file <retained project-relative JSON> [<dir>]
|
|
16
|
+
Retain citations inside the project, for example evidence/scene-src/citations.json; absolute paths are refused.
|
|
16
17
|
Citations are a role map {background: citation, foreground?: citation}.
|
|
17
18
|
Each citation names post_composition, asks:["post_composition"], and quote or delegated:true.
|
|
18
19
|
Paths and rects must match the current ingested post_composition choice.
|