@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.
Files changed (117) hide show
  1. package/README.md +31 -23
  2. package/docs/CHECKS.md +9 -8
  3. package/docs/INTERVIEW.md +11 -4
  4. package/docs/LIVE-REVIEW.md +525 -0
  5. package/package.json +3 -2
  6. package/src/agent-guide.ts +16 -13
  7. package/src/choice-writers.ts +8 -2
  8. package/src/choices.ts +1 -1
  9. package/src/cli.ts +9 -1
  10. package/src/direction-writers.ts +3 -1
  11. package/src/direction.ts +2 -1
  12. package/src/feedback.ts +5 -2
  13. package/src/init.ts +1 -1
  14. package/src/job-acts.ts +44 -3
  15. package/src/job-host.ts +1 -1
  16. package/src/next.ts +113 -53
  17. package/src/record-input.ts +1 -1
  18. package/src/reopen.ts +1 -1
  19. package/src/review-package.ts +2 -1
  20. package/src/scene-writers.ts +2 -1
  21. package/src/scene.ts +3 -1
  22. package/src/signoff-pair.ts +60 -0
  23. package/src/signoff-target.ts +7 -4
  24. package/src/signoff.ts +5 -0
  25. package/src/structure.ts +1 -1
  26. package/templates/agent-guide.md +29 -31
  27. package/templates/example-chain/AGENTS.md +41 -43
  28. package/templates/example-chain/chain.json +55 -55
  29. package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +41 -43
  30. package/templates/example-chain/checkpoints/0a-interview-options.json +1 -1
  31. package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +41 -43
  32. package/templates/example-chain/checkpoints/0b-plan-pending.json +1 -1
  33. package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +41 -43
  34. package/templates/example-chain/checkpoints/1-parts-verdict/project.json +2 -2
  35. package/templates/example-chain/checkpoints/1-parts-verdict.json +5 -5
  36. package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +41 -43
  37. package/templates/example-chain/checkpoints/2-rigging-answers/project.json +2 -2
  38. package/templates/example-chain/checkpoints/2-rigging-answers.json +5 -5
  39. package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +41 -43
  40. package/templates/example-chain/checkpoints/3-rig-review/project.json +4 -4
  41. package/templates/example-chain/checkpoints/3-rig-review.json +9 -9
  42. package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +41 -43
  43. package/templates/example-chain/checkpoints/4-direction-post/project.json +5 -5
  44. package/templates/example-chain/checkpoints/4-direction-post.json +9 -9
  45. package/templates/example-chain/evidence/captions.json +1 -1
  46. package/templates/example-chain/evidence/direction/v2.json +1 -1
  47. package/templates/example-chain/evidence/direction-input.json +1 -1
  48. package/templates/example-chain/evidence/scene/s1.json +1 -1
  49. package/templates/example-chain/project.json +10 -10
  50. package/templates/example-direction/derived/review/v1/out/check/build/run.json +2 -2
  51. package/templates/example-draft/derived/drafts/lantern/out/check/build/run.json +2 -2
  52. package/templates/example-draft/derived/drafts/lantern/out/run.json +2 -2
  53. package/templates/example-draft/derived/review/v1/out/check/build/run.json +2 -2
  54. package/templates/example-draft/derived/review/v1/out/rig/run.json +2 -2
  55. package/templates/example-draft/derived/review/v2/out/check/build/run.json +2 -2
  56. package/templates/example-draft/derived/review/v2/out/rig/run.json +2 -2
  57. package/templates/example-draft/evidence/direction/v4.json +3 -3
  58. package/templates/example-draft/evidence/direction-input-v4.json +3 -3
  59. package/templates/example-draft/evidence/parts-review.json +1 -1
  60. package/templates/example-draft/evidence/review-reads/{4ed0507381809746443021554e012032be5b5f8417da0f2e525e9b6df16e9f05.json → 52673b635b9cbc92da1a9e3d4c023a249bbacf79cd21b59724f03f18f8caf09b.json} +2 -2
  61. package/templates/example-draft/evidence/scene/s1.json +3 -3
  62. package/templates/example-draft/evidence/scene-citations.json +1 -1
  63. package/templates/example-draft/out/check/build/run.json +2 -2
  64. package/templates/example-draft/out/run.json +2 -2
  65. package/templates/example-draft/outputs/v1/out/check/build/run.json +2 -2
  66. package/templates/example-draft/outputs/v1/out/rig/run.json +2 -2
  67. package/templates/example-draft/outputs/v2/out/check/build/run.json +2 -2
  68. package/templates/example-draft/outputs/v2/out/rig/run.json +2 -2
  69. package/templates/example-interview-choice.json +68 -68
  70. package/templates/example-interview-options/constraint.json +2 -2
  71. package/templates/example-interview-options/example-assemble.json +2 -2
  72. package/templates/example-interview-options/example-bones.json +5 -5
  73. package/templates/example-interview-options/example-camera-depth.json +3 -3
  74. package/templates/example-interview-options/example-constraints.json +5 -5
  75. package/templates/example-interview-options/example-meshes.json +5 -5
  76. package/templates/example-interview-options/example-motion.json +5 -5
  77. package/templates/example-interview-options/example-picks.json +2 -2
  78. package/templates/example-interview-options/example-post-composition.json +3 -3
  79. package/templates/example-interview-options/example-post-timing.json +3 -3
  80. package/templates/example-interview-options/example-reference.json +2 -2
  81. package/templates/example-interview-options/example-rig-type.json +2 -2
  82. package/templates/example-interview-options/example-seethrough.json +2 -2
  83. package/templates/example-interview-options/fullscreen.json +2 -2
  84. package/templates/example-interview-options/other-explicit.json +2 -2
  85. package/templates/example-interview-options/other-pending.json +2 -2
  86. package/templates/example-interview-options/other-picks.json +2 -2
  87. package/templates/example-invariants/invariants.json +164 -164
  88. package/templates/example-project.json +249 -249
  89. package/templates/example-remesh/AGENTS.md +41 -43
  90. package/templates/example-remesh/evidence/captions.json +1 -1
  91. package/templates/example-remesh/evidence/direction/v4.json +1 -1
  92. package/templates/example-remesh/evidence/direction/v5.json +1 -1
  93. package/templates/example-remesh/evidence/direction-input-v4.json +1 -1
  94. package/templates/example-remesh/evidence/direction-input-v5.json +1 -1
  95. package/templates/example-remesh/evidence/interview/assemble.json +1 -1
  96. package/templates/example-remesh/evidence/interview/bones.json +4 -4
  97. package/templates/example-remesh/evidence/interview/camera_depth.json +2 -2
  98. package/templates/example-remesh/evidence/interview/constraints.json +4 -4
  99. package/templates/example-remesh/evidence/interview/meshes.json +4 -4
  100. package/templates/example-remesh/evidence/interview/motion.json +4 -4
  101. package/templates/example-remesh/evidence/interview/post_composition.json +2 -2
  102. package/templates/example-remesh/evidence/interview/post_timing.json +2 -2
  103. package/templates/example-remesh/evidence/interview/reference.json +1 -1
  104. package/templates/example-remesh/evidence/interview/rig_type.json +1 -1
  105. package/templates/example-remesh/evidence/interview/seethrough.json +1 -1
  106. package/templates/example-remesh/evidence/parts-review.json +1 -1
  107. package/templates/example-remesh/evidence/review-reads/{6a17db694db96f1b6049a61a017e0c8e66757b982b8851e0187e561cfcf9af68.json → afe2522be4087f7f2fa7e97746d5f8574b0aedb474245ce2dbce0455e662f041.json} +2 -2
  108. package/templates/example-remesh/evidence/scene/s1.json +2 -2
  109. package/templates/example-remesh/evidence/v1/registration/at-source.json +2 -2
  110. package/templates/example-remesh/evidence/v1/registration/rigc-run-1.json +2 -2
  111. package/templates/example-remesh/evidence/v2/registration/at-source.json +2 -2
  112. package/templates/example-remesh/evidence/v2/registration/rigc-run-1.json +2 -2
  113. package/templates/example-remesh/evidence/v3/registration/at-source.json +2 -2
  114. package/templates/example-remesh/evidence/v3/registration/rigc-run-1.json +2 -2
  115. package/templates/example-remesh/project.json +225 -225
  116. package/templates/example-reopened/project.json +289 -289
  117. 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', 'retain the accepted rig parts identity before drafting post options');
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
 
@@ -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 === '..') || !boundOutput && structurePathIsIgnored(dir, path) && !/^derived\/review\/v[1-9][0-9]*\/out\/check\/build\/skeleton\.model\.json$/.test(path)) throw Error('literal retained POSIX path required');
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}; one artwork is one project, so record a redo, a new source or a new tool as a version there, or give --same-artwork-reason <file> for a different rig kind`);
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', 'project.json: requested question has no unique pending option set; register it with interview-options first');
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}/parts.json${figureFlag} --floor 2 --threshold 40 --out evidence/diff-components.json`,
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 'rig-create diff-classes --diff-report evidence/diff-components.json --by agent';
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 'rig-create diff-classes --diff-report evidence/diff-components.json --triage-file evidence/diff-triage.json --by agent';
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 warningFields = structureWarnings.length ? { warnings: structureWarnings } : {};
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: 'human',
860
+ role: 'agent',
820
861
  field: answerField,
821
862
  awaits: answerField,
822
- command: null,
823
- act: `review-tool act interview-picks for ${questionPath} (unanswered by record: ${questionPath}.pending_options is not recorded in project.json)`,
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 needsPlanSteps =
848
- (effectiveStage === 'interview' && steps.length === 0) ||
849
- (effectiveStage === 'post-interview' && !steps.some(s => s.phase === 'post')) ||
850
- (effectiveStage === 'rigging-interview' && !steps.some(step => stepCoveredByApproval(project, step, effectiveStage)));
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
- if (effectiveStage === 'rigging-interview') {
854
- for (const id of ['bones', 'meshes', 'constraints', 'motion']) {
855
- const item = stageItems.find(item => item.id === id);
856
- if (!item || item.state.state === 'silent' || (item.state.state === 'delegated' && !item.state.decided) || steps.some(step => step.id === id)) continue;
857
- moves.push({
858
- role: 'agent',
859
- field: 'plan.steps',
860
- awaits: 'plan.steps',
861
- command: `rig-create plan add ${id} '${dir.replaceAll("'", "'\\''")}' --by agent --phase rig …`,
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 hasStructureFile =
1030
- existsSync(join(dir, 'evidence/structure.json')) ||
1031
- existsSync(join(dir, 'evidence/structure-decisions.json'));
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: 'evidence/structure.json',
1038
- command: `rig-create structure-snapshot --draft ${draftId} --out evidence/structure.json`,
1039
- act: `write structure-snapshot for drafts[${draftId}] to evidence/structure.json`,
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
- ? 'review-tool act signoff for stage parts-review'
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'}], evidence/structure.json`,
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 (scenes.length === 0) {
1340
- addAwait('scenes[n=1]');
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]`;
@@ -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.`;
@@ -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 is unavailable (${finding.code}); restore its recorded API response and selected draft before packaging this version` });
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 });
@@ -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 <file> [<dir>]
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.