@rig-c/create 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/CHECKS.md +8 -1
- package/docs/INTERVIEW.md +20 -0
- package/package.json +1 -1
- package/src/check.ts +10 -0
- package/src/choice-writers.ts +6 -1
- package/src/cli.ts +3 -2
- package/src/job-close.ts +6 -2
- package/src/job-host.ts +1 -1
- package/src/job-outbox.ts +12 -0
- package/src/job-publication.ts +22 -4
- package/src/job-runtime.ts +2 -2
- package/src/next.ts +19 -0
- package/src/owner-chat.ts +67 -0
- package/src/signoff.ts +10 -3
- package/templates/agent-guide.md +1 -1
- package/templates/example-chain/AGENTS.md +1 -1
- package/templates/example-chain/checkpoints/0a-interview-options/AGENTS.md +1 -1
- package/templates/example-chain/checkpoints/0a-interview-options.json +1 -1
- package/templates/example-chain/checkpoints/0b-plan-pending/AGENTS.md +1 -1
- package/templates/example-chain/checkpoints/0b-plan-pending.json +1 -1
- package/templates/example-chain/checkpoints/1-parts-verdict/AGENTS.md +1 -1
- package/templates/example-chain/checkpoints/1-parts-verdict.json +1 -1
- package/templates/example-chain/checkpoints/2-rigging-answers/AGENTS.md +1 -1
- package/templates/example-chain/checkpoints/2-rigging-answers.json +1 -1
- package/templates/example-chain/checkpoints/3-rig-review/AGENTS.md +1 -1
- package/templates/example-chain/checkpoints/3-rig-review.json +1 -1
- package/templates/example-chain/checkpoints/4-direction-post/AGENTS.md +1 -1
- package/templates/example-chain/checkpoints/4-direction-post.json +1 -1
- package/templates/example-remesh/AGENTS.md +1 -1
package/docs/CHECKS.md
CHANGED
|
@@ -24,7 +24,7 @@ requires Bun 1.4 or later. See [Live review](LIVE-REVIEW.md).
|
|
|
24
24
|
|
|
25
25
|
| Code | Finding and remedy |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
| `JOB_PUBLISH_PENDING` | A committed `project.json` result awaits publication. Restore its bound host and sources, then run `job reconcile` before another participating write or close. |
|
|
27
|
+
| `JOB_PUBLISH_PENDING` | A committed `project.json` result awaits publication. Restore its bound host and sources, then run `job reconcile` before another participating write or close. To abandon failed publications explicitly, `job close --retire-refused` archives their complete receipts under the private `retired-publications/` directory and closes without acknowledging them; unfailed pending publications still block close. |
|
|
28
28
|
| `JOB_ROOT_REPLACED` | `project.json`: the managed Review state belongs to a replaced project directory, or its ownership/liveness cannot be established safely. Stop the original `job open` host in its terminal, then retry `job open` or `job reconcile`. With no pending publication or startup claim, both commands preserve the old state in a dated `.replaced-…` sibling only when its recorded PID is absent and its recorded loopback port refuses connections. Missing/unsafe metadata, an occupied port or uncertain liveness refuses without retiring state; restore the retained metadata or original directory before retrying. |
|
|
29
29
|
| `JOB_HOST_ATTACH_REFUSED` | Strict reuse-only attach to the bound live host was refused. The pending receipt names the host `reason` and `phase` (including `incarnation-missing` on `record` or `probe`). Reopen or restore the bound live host before reconciling. |
|
|
30
30
|
| `JOB_HOST_REUSE_MISMATCH` | The private binding's host tuple or `listenerIncarnation` (`jobId`, `origin`, `listenerIncarnation`, `root`, `stateDir`, `fingerprint`, `reviewer.id`, `reviewer.role`, `readerId`, `bindingId`, `partsEnumeratorId`, `partsEnumeratorVersion`, `apiSchema`, `actSchema`, `port`) differs. The pending receipt names the `field` and both `expected` and `actual` values. Restore the original host configuration before reconciling. |
|
|
@@ -1717,3 +1717,10 @@ checks remain in force; no source measurement is recomputed.
|
|
|
1717
1717
|
new placement. Re-signing or adopting the split order on a later date alone
|
|
1718
1718
|
does not retrofit it into historical interviews. Missing `turn` on an adopted
|
|
1719
1719
|
interview is the existing `WAYPOINT_MISSING` refusal.
|
|
1720
|
+
|
|
1721
|
+
### Relayed owner chat
|
|
1722
|
+
|
|
1723
|
+
| Code | Meaning and remedy |
|
|
1724
|
+
| --- | --- |
|
|
1725
|
+
| `OWNER_CHAT_INVALID` | The named project.json answer/signoff lacks exact nonblank owner words, a valid explicit ISO said_at, relay identity, or the relayed source, or mixes chat and Review flags. Supply `--owner-chat`, `--said-at`, `--relayed-by` and owner `--by` together; keep at as the write time and omit act ids. |
|
|
1726
|
+
| `OWNER_CHAT_GATE_VISUAL` | The named project.json stage signoff has no accepted live verdict for the current draft, rig or post. Get the verdict first in the review tool with `job open --draft <id>` or `job open --rig <n>`, ingest it, then relay the signoff. |
|
package/docs/INTERVIEW.md
CHANGED
|
@@ -903,3 +903,23 @@ screens a corpus reproduction away. A new report needs a fresh shown set
|
|
|
903
903
|
and explicit choice; the old choice and set remain history. Choices are
|
|
904
904
|
local, never inherited from a collection. They may be recorded only in
|
|
905
905
|
parts, but later signatures do not make a pending optional choice invalid.
|
|
906
|
+
|
|
907
|
+
### Relayed owner chat
|
|
908
|
+
|
|
909
|
+
All interview questions may use `interview-answer --owner-chat <words file>
|
|
910
|
+
--said-at <ISO date> --relayed-by <operator> --by <owner>` alongside the
|
|
911
|
+
existing `--set`, `--sha256` and explicit `--pick` flags. Shared context images
|
|
912
|
+
are background and do not change this route. The writer retains generated
|
|
913
|
+
pick text in `verbatim`, exact UTF-8 chat in `owner_words`, the owner's original
|
|
914
|
+
`said_at`, the relay's `relayed_by`, and source `owner chat, relayed`.
|
|
915
|
+
`at` remains the write time; no review act id is created. Empty or whitespace
|
|
916
|
+
words, missing clocks or relay identity, and agent owners are refused.
|
|
917
|
+
If options ever carry their own images, those sets leave the chat path;
|
|
918
|
+
that requires a new Review contract outside this change.
|
|
919
|
+
|
|
920
|
+
Stage signoffs use the same flags, retaining the exact words in `verbatim`.
|
|
921
|
+
Parts-review, rig-review and direction-review first require the current
|
|
922
|
+
accepted live verdict from the review tool; changes-requested does not count.
|
|
923
|
+
Get that verdict with `job open --draft <id>` (parts) or `job open --rig <n>`
|
|
924
|
+
(rig/post), then ingest it through the existing verdict writer. Verdict writers
|
|
925
|
+
have no owner-chat route. Review remains available for picks and signoffs.
|
package/package.json
CHANGED
package/src/check.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownerChatFinding } from './owner-chat.ts';
|
|
1
2
|
import { packageUpgradeFindings } from './package-upgrades.ts';
|
|
2
3
|
import { checkPublicationGenerations } from './job-publication.ts';
|
|
3
4
|
import { collectBarAmendments, effectiveBar } from './plan-bar-amendments.ts';
|
|
@@ -1001,6 +1002,15 @@ function checkProjectOnce(projectDirArg: string, candidate: JsonObject | undefin
|
|
|
1001
1002
|
if (!hasStageOrder(project)) warnings.push(STAGE_ORDER_LEGACY_FINDING);
|
|
1002
1003
|
checkInputs(ctx);
|
|
1003
1004
|
checkChoices(ctx);
|
|
1005
|
+
const checkChat = (value: Json | undefined, path: string): void => {
|
|
1006
|
+
if (Array.isArray(value)) value.forEach((entry, index) => checkChat(entry, `${path}[${index}]`));
|
|
1007
|
+
else if (isObject(value)) {
|
|
1008
|
+
const finding = ownerChatFinding(value, path);
|
|
1009
|
+
if (finding) ctx.refusals.push(finding);
|
|
1010
|
+
for (const [key, entry] of Object.entries(value)) checkChat(entry, `${path}.${key}`);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
checkChat(project, 'project.json');
|
|
1004
1014
|
if (!options.recordPublication) checkCorrections(ctx, options.now);
|
|
1005
1015
|
if (!options.recordPublication) checkScenes(ctx);
|
|
1006
1016
|
if (!options.recordPublication) checkDirections(ctx, options.now);
|
package/src/choice-writers.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OWNER_CHAT_FLAGS, readOwnerChat } from './owner-chat.ts';
|
|
1
2
|
import { overrideFindings } from './overrides.ts';
|
|
2
3
|
import { intakeItem, intakeWarnings, INTAKE_QUESTION } from './intake.ts';
|
|
3
4
|
/** Public record writers; validation precedes every project write. */
|
|
@@ -20,6 +21,7 @@ interview-options --adopt [<dir>]
|
|
|
20
21
|
interview-answer <question> [<dir>] --from-review <actId> [--why-file <file>]
|
|
21
22
|
interview-answer <question> [<dir>] --set <path> --sha256 <shown sha> --by <human>
|
|
22
23
|
[--pick <ask>=<option>...] [--other <ask>=<file> --other-fragment <ask>=<JSON file>]...
|
|
24
|
+
[--owner-chat <words file> --said-at <ISO date> --relayed-by <who>]
|
|
23
25
|
[--note-file <file>] [--source <location>] [--why-file <file> for a member override]
|
|
24
26
|
interview-decision <question> [<dir>] --by <agent> --reasoning-file <file>
|
|
25
27
|
--fragment <ask>=<JSON file>...
|
|
@@ -139,8 +141,10 @@ export interface InterviewAnswerHooks {
|
|
|
139
141
|
}
|
|
140
142
|
export function interviewAnswerCommand(args: string[], now: number, hooks: InterviewAnswerHooks = {}): string { return run(() => {
|
|
141
143
|
if (args.includes('--picks-file')) refuse('INTERVIEW_PICKS_FILE_REMOVED', 'project.json: --picks-file was removed; use interview-answer <question> --from-review <actId>', USAGE_EXIT);
|
|
142
|
-
const opts = parse(args,['--set','--sha256','--by','--note-file','--source','--from-review','--why-file'],['--pick','--other','--other-fragment']);
|
|
144
|
+
const opts = parse(args,['--set','--sha256','--by','--note-file','--source','--from-review','--why-file',...OWNER_CHAT_FLAGS],['--pick','--other','--other-fragment']);
|
|
143
145
|
if (opts.positional.length > 2 || (!opts.one['--from-review'] && (!opts.one['--set'] || !opts.one['--sha256'] || !opts.one['--by']))) usage('question and --from-review, or --set/--sha256/--by required');
|
|
146
|
+
if (opts.one['--owner-chat'] !== undefined && opts.one['--by']?.toLowerCase() === 'agent') refuse('INTERVIEW_CHOICE_INVALID', 'project.json.interview: --by must name the owner; relay owner words without claiming agent authority');
|
|
147
|
+
const chat = readOwnerChat(opts.one, opts.one['--by'] ?? '', now, 'project.json.interview');
|
|
144
148
|
const dir = resolve(opts.positional[1] ?? process.cwd());
|
|
145
149
|
const release = acquireBoundJobWrite(dir);
|
|
146
150
|
try {
|
|
@@ -208,6 +212,7 @@ export function interviewAnswerCommand(args: string[], now: number, hooks: Inter
|
|
|
208
212
|
else if (opts.one['--note-file']) choice.note = readText(opts.one['--note-file']);
|
|
209
213
|
const result = selection(set,choice);
|
|
210
214
|
const answer: JsonObject = {delegated:result.delegated,by:payload?.by ?? opts.one['--by'],at:payload?.at ?? new Date(now).toISOString(),verbatim:choiceVerbatim(picks,choice.note),source:opts.one['--source'] ?? 'review questionnaire',choice,asks:result.asks};
|
|
215
|
+
if (chat) { answer.owner_words = chat.owner_words!; answer.said_at = chat.said_at!; answer.relayed_by = chat.relayed_by!; answer.source = chat.source!; }
|
|
211
216
|
if (result.value !== undefined) answer.value = result.value;
|
|
212
217
|
if (result.autos.length) answer.agent_decision = {covers:result.autos.join(',')};
|
|
213
218
|
if (isObject(item.raw) && typeof item.raw.basis === 'string') answer.basis = item.raw.basis === 'inherited' ? (item.state.state === 'silent' ? 'own' : 'overridden') : item.raw.basis;
|
package/src/cli.ts
CHANGED
|
@@ -51,7 +51,7 @@ Usage:
|
|
|
51
51
|
rig-create republish <rig> [<dir>] --reason <words>
|
|
52
52
|
rig-create job open [<dir>] --reviewer <id> --role owner|delegate [--draft <id> | --rig <n> | --question <question> | --plan]
|
|
53
53
|
rig-create job acts [<dir>] [--json] list finalized acts and their ingest commands
|
|
54
|
-
rig-create job reconcile|close [<dir>]
|
|
54
|
+
rig-create job reconcile|close [<dir>] [--retire-refused (close only)]
|
|
55
55
|
${INPUT_CLI} rig-create init <dir> [--no-install] create a project folder, its repository and pinned packages
|
|
56
56
|
rig-create init <dir> --painting <file> --origin <words> [--projects-root <dir>] [--same-artwork-reason <file>]
|
|
57
57
|
also register the painting; refuse a second project for one artwork
|
|
@@ -310,6 +310,7 @@ export function main(argv: string[], now: number): number {
|
|
|
310
310
|
if (!value || value.startsWith('-') || options[arg]) refuse('JOB_BINDING_INVALID', 'project.json: job open requires one value for each reviewer option', USAGE_EXIT);
|
|
311
311
|
options[arg] = value;
|
|
312
312
|
} else if (action === 'open' && arg === '--plan' && !options[arg]) options[arg] = 'true';
|
|
313
|
+
else if (action === 'close' && arg === '--retire-refused' && !options[arg]) options[arg] = 'true';
|
|
313
314
|
else if (action === 'acts' && arg === '--json' && !options[arg]) options[arg] = 'true';
|
|
314
315
|
else if (arg.startsWith('-')) refuse('JOB_BINDING_INVALID', 'project.json: unsupported job option; see help', USAGE_EXIT);
|
|
315
316
|
else positional.push(arg);
|
|
@@ -320,7 +321,7 @@ export function main(argv: string[], now: number): number {
|
|
|
320
321
|
if (['--draft', '--rig', '--question', '--plan'].filter(flag => options[flag] !== undefined).length > 1) {
|
|
321
322
|
refuse('JOB_BINDING_INVALID', 'project.json: job open takes one of --draft, --rig, --question or --plan; choose one recorded result', USAGE_EXIT);
|
|
322
323
|
}
|
|
323
|
-
const stdin = Buffer.from(JSON.stringify({ root: positional[0] ?? process.cwd(), reviewer: options['--reviewer'], role: options['--role'], draft: options['--draft'], rig: options['--rig'], question: options['--question'], plan: options['--plan'] === 'true', json: options['--json'] === 'true', now }));
|
|
324
|
+
const stdin = Buffer.from(JSON.stringify({ root: positional[0] ?? process.cwd(), reviewer: options['--reviewer'], role: options['--role'], draft: options['--draft'], rig: options['--rig'], question: options['--question'], plan: options['--plan'] === 'true', json: options['--json'] === 'true', retireRefused: options['--retire-refused'] === 'true', now }));
|
|
324
325
|
const runtime = join(import.meta.dir, 'job-runtime.ts');
|
|
325
326
|
if (action === 'open') {
|
|
326
327
|
const child = Bun.spawn(jobRuntimeCommand(action, runtime), {
|
package/src/job-close.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
acquireJobLock,
|
|
14
14
|
JOB_BINDING_FILE,
|
|
15
15
|
pendingPublications,
|
|
16
|
+
retireRefusedPublications,
|
|
16
17
|
readWriterBinding,
|
|
17
18
|
type JobIdentity,
|
|
18
19
|
type WriterBinding,
|
|
@@ -181,12 +182,14 @@ export async function attachCreateJobForClose(
|
|
|
181
182
|
export async function closeCreateJob(
|
|
182
183
|
root: string,
|
|
183
184
|
startHost: (options: JobHostOptions) => Promise<JobOwnerHost> = startJobHost,
|
|
185
|
+
retireRefused = false,
|
|
184
186
|
): Promise<void> {
|
|
185
187
|
root = realpathSync(root);
|
|
186
188
|
const release = acquireJobLock(root)!;
|
|
187
189
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
const pending = pendingPublications(root);
|
|
191
|
+
if (pending.length && (!retireRefused || pending.some(entry => !entry.failure))) {
|
|
192
|
+
refuse('JOB_PUBLISH_PENDING', 'project.json: a committed publication remains pending; run rig-create job reconcile, or job close --retire-refused to archive failed publications and close without publishing them');
|
|
190
193
|
}
|
|
191
194
|
const binding = readWriterBinding(root);
|
|
192
195
|
if (!binding) refuse('JOB_BINDING_INVALID', 'project.json: no writer-owned job binding; open the live job first');
|
|
@@ -216,6 +219,7 @@ export async function closeCreateJob(
|
|
|
216
219
|
if (!current || !isDeepStrictEqual(current, binding)) {
|
|
217
220
|
refuse('JOB_BINDING_INVALID', 'project.json: private live-job binding changed during close; restore the writer-owned binding');
|
|
218
221
|
}
|
|
222
|
+
if (retireRefused) retireRefusedPublications(root, pending);
|
|
219
223
|
rmSync(join(root, JOB_BINDING_FILE));
|
|
220
224
|
} finally {
|
|
221
225
|
host.stop();
|
package/src/job-host.ts
CHANGED
|
@@ -369,7 +369,7 @@ async function reconcileLocked(root: string, publish: (entry: PendingPublication
|
|
|
369
369
|
const failure = error instanceof Refusal ? { code: error.code, message: error.message }
|
|
370
370
|
: { code: 'JOB_PUBLISH_PENDING', message: 'project.json: host publication failed; restore the live host before rig-create job reconcile' };
|
|
371
371
|
failPublication(root, entry, failure);
|
|
372
|
-
refuse('JOB_PUBLISH_PENDING', `project.json: record ${entry.recordHash} remains pending (${failure.code}: ${failure.message}); run rig-create job reconcile`);
|
|
372
|
+
refuse('JOB_PUBLISH_PENDING', `project.json: record ${entry.recordHash} remains pending (${failure.code}: ${failure.message}); run rig-create job reconcile after restoring the sources, or job close --retire-refused to archive failed publications and close without publishing them`);
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
return { count: entries.length, outcomes };
|
package/src/job-outbox.ts
CHANGED
|
@@ -302,6 +302,18 @@ export function failPublication(root: string, entry: PendingPublication, failure
|
|
|
302
302
|
writePrivateJson(pendingPath(root, entry.id), { ...entry, failure });
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/** Explicit close recovery retains the complete failed receipt; it never acknowledges publication. */
|
|
306
|
+
export function retireRefusedPublications(root: string, entries: PendingPublication[]): void {
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
if (!entry.failure) refuse('JOB_PUBLISH_PENDING', 'project.json: an unfailed publication cannot be retired; run rig-create job reconcile first');
|
|
309
|
+
const directory = join(jobPrivateRoot(root), 'retired-publications');
|
|
310
|
+
privateDirectory(directory);
|
|
311
|
+
writePrivateJson(join(directory, `${entry.id}.json`), entry);
|
|
312
|
+
rmSync(pendingPath(root, entry.id));
|
|
313
|
+
syncDirectory(join(jobPrivateRoot(root), 'outbox'));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
305
317
|
export async function reconcilePublication(root: string, entry: PendingPublication,
|
|
306
318
|
publish: (entry: PendingPublication) => Promise<unknown>): Promise<void> {
|
|
307
319
|
if (currentVersionHash(root, entry) !== entry.versionHash) {
|
package/src/job-publication.ts
CHANGED
|
@@ -328,8 +328,13 @@ export function readRecordPublished({ root, record, result, phase, parts: sealed
|
|
|
328
328
|
return { label, parts, inputs, files, ...evidence,
|
|
329
329
|
plan: { steps: steps.map(step => ({ id: String(step.id), hash: stepHash(step) })) }, subjects };
|
|
330
330
|
} catch (error) {
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
const failure = error instanceof Refusal && (phase === 'retained' || ['JOB_RECORD_SUBJECT_INVALID', 'JOB_RECORD_COLLECTION_EXTERNAL', 'JOB_RECORD_PARTS_LOCK_MISSING', 'JOB_DIRECTION_VERDICT_REQUIRED', 'REVIEW_ACT_RECORD_TARGET_INVALID', 'REVIEW_ACT_TARGET_MISMATCH', 'INTERVIEW_SIGNOFF_NOT_CLEAN', 'INTERVIEW_SIGNOFF_PLAN_REQUIRED'].includes(error.code))
|
|
332
|
+
? error
|
|
333
|
+
: new Refusal('JOB_RECORD_INCOMPLETE', `project.json: ${error instanceof Error ? error.message : String(error)}; restore the requested record and retained evidence, resolve check refusals and reconcile pending publications before publishing`);
|
|
334
|
+
// Review returns only a safe producer token. Its retained root is private and may
|
|
335
|
+
// be removed on refusal, so report here in the host terminal before it is lost.
|
|
336
|
+
if (phase === 'retained') console.error(`${failure.code}: ${failure.message}`);
|
|
337
|
+
throw failure;
|
|
333
338
|
}
|
|
334
339
|
}
|
|
335
340
|
|
|
@@ -608,8 +613,21 @@ export function prepareDraftManifest(root: string, project: JsonObject, id: stri
|
|
|
608
613
|
if (existsSync(join(root, path))) {
|
|
609
614
|
const existing = publicationFile(root, path);
|
|
610
615
|
if (!existing.equals(bytes)) {
|
|
611
|
-
|
|
612
|
-
|
|
616
|
+
let orphaned = false;
|
|
617
|
+
try {
|
|
618
|
+
const retained: unknown = JSON.parse(existing.toString('utf8'));
|
|
619
|
+
if (isObject(retained as Json)) {
|
|
620
|
+
const prior = retained as JsonObject;
|
|
621
|
+
const revisions = asArray(project.drafts).filter(isObject).map(item => jobHash(`${JSON.stringify(item, null, 2)}\n`));
|
|
622
|
+
orphaned = prior.schema === 'rig-review/1' && prior.id === `create-draft-${id}`
|
|
623
|
+
&& typeof prior.revision === 'string' && /^[a-f0-9]{64}$/.test(prior.revision)
|
|
624
|
+
&& !revisions.includes(prior.revision)
|
|
625
|
+
&& !pendingPublications(root).some(entry => !isRecordResult(entry.result)
|
|
626
|
+
&& entry.result.path === path && entry.versionHash === prior.revision);
|
|
627
|
+
}
|
|
628
|
+
} catch { /* An unreadable manifest is never evidence of an orphaned revision. */ }
|
|
629
|
+
if (!existing.equals(legacyPanelsBytes) && !existing.equals(legacyBytes) && !orphaned) {
|
|
630
|
+
refuse('JOB_PUBLICATION_INVALID', `drafts[${id}]: retained publication manifest differs; restore it before reconciling, or run rig-create job close --retire-refused to retire a refused publication`);
|
|
613
631
|
}
|
|
614
632
|
const target = join(root, path);
|
|
615
633
|
const tmp = `${target}.${process.pid}-${crypto.randomUUID()}.tmp`;
|
package/src/job-runtime.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function publicationFailure(error: unknown, root: string): Refusal | unde
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async function run(): Promise<void> {
|
|
20
|
-
const input = JSON.parse(readFileSync(0, 'utf8')) as { root: string; id?: string; reviewer?: string; role?: 'owner' | 'delegate'; draft?: string; rig?: string; question?: string; plan?: boolean; now: number; json?: boolean };
|
|
20
|
+
const input = JSON.parse(readFileSync(0, 'utf8')) as { root: string; id?: string; reviewer?: string; role?: 'owner' | 'delegate'; draft?: string; rig?: string; question?: string; plan?: boolean; now: number; retireRefused?: boolean; json?: boolean };
|
|
21
21
|
const root = resolve(input.root);
|
|
22
22
|
switch (process.argv[2]) {
|
|
23
23
|
case 'publish': {
|
|
@@ -43,7 +43,7 @@ async function run(): Promise<void> {
|
|
|
43
43
|
break;
|
|
44
44
|
}
|
|
45
45
|
case 'reconcile': console.log(`job: reconciled ${await reconcileCreateJob(root, undefined, input.now)} publication(s)`); break;
|
|
46
|
-
case 'close': await closeCreateJob(root); console.log('job: closed'); break;
|
|
46
|
+
case 'close': await closeCreateJob(root, undefined, input.retireRefused); console.log('job: closed'); break;
|
|
47
47
|
case 'open': {
|
|
48
48
|
if (!input.reviewer || !input.role) refuse('JOB_BINDING_INVALID', 'project.json: job open needs --reviewer and --role owner|delegate');
|
|
49
49
|
requestedRecordResult(root, input.question, input.plan, input.now);
|
package/src/next.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { optionFile } from './choices.ts';
|
|
2
|
+
import { ownerChatSignoffAllowed } from './owner-chat.ts';
|
|
1
3
|
import { checkpointEnabled, checkpointRequired } from './plan-history.ts';
|
|
2
4
|
import { createHash } from 'node:crypto';
|
|
3
5
|
import { existsSync, readFileSync } from './check-reads.ts';
|
|
@@ -238,7 +240,24 @@ function reviewCommands(dir: string, project: JsonObject, move: NextMove): NextM
|
|
|
238
240
|
role: 'agent', field: move.field, awaits: move.awaits, command, act: description,
|
|
239
241
|
});
|
|
240
242
|
const root = shellWord(dir);
|
|
243
|
+
const chatMoves: NextMove[] = [];
|
|
244
|
+
const chatFlags = "--by '<owner>' --owner-chat '<words file>' --said-at '<ISO date>' --relayed-by '<operator>'";
|
|
245
|
+
if (act.kind === 'signoff' && ownerChatSignoffAllowed(dir, project, act.stage, act.draft_id, act.version_id?.slice(1))) {
|
|
246
|
+
const version = act.stage === 'rig-review' || act.stage === 'direction-review' ? ` --version ${shellWord(act.version_id?.slice(1) ?? '<current-version>')}` : '';
|
|
247
|
+
chatMoves.push({...move, command:`rig-create ${writer} ${root}${version} ${chatFlags}`, act:`${move.act}; or relay the owner chat signoff with exact words, owner date and relay identity`, review_act:undefined});
|
|
248
|
+
} else if (act.kind === 'interview-picks' && act.question_id) {
|
|
249
|
+
const item = collectItems(project, {collection:loadCollection(dir)}).find(item => item.id === act.question_id);
|
|
250
|
+
const binding = item?.record.pending_options;
|
|
251
|
+
if (isObject(binding) && typeof binding.path === 'string' && typeof binding.sha256 === 'string') {
|
|
252
|
+
try {
|
|
253
|
+
const {set} = optionFile(dir,binding);
|
|
254
|
+
const picks = asArray(set.asks).filter(isObject).map(ask => ` --pick ${shellWord(`${String(ask.id)}=<option-id>`)}`).join('');
|
|
255
|
+
chatMoves.push({...move, command:`rig-create ${writer} ${root} --set ${shellWord(binding.path)} --sha256 ${shellWord(binding.sha256)} ${chatFlags}${picks}`, act:`${move.act}; or relay the owner chat answer and explicit option ids from the displayed set`, review_act:undefined});
|
|
256
|
+
} catch { /* The check's named option-file refusal supplies the repair route. */ }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
241
259
|
return [
|
|
260
|
+
...chatMoves,
|
|
242
261
|
agent(`rig-create job open ${root} ${selector} --reviewer '<reviewer>' --role owner`, 'host this Review page; keep the host running in a separate terminal until job close'),
|
|
243
262
|
move,
|
|
244
263
|
agent(`rig-create job acts ${root} --json`, `after the person submits in Review, list exported acts and use the matching actId below${act.stage === 'parts-review' ? '; run its reported preconditions before ingestion (parts-review signoff includes structure-snapshot)' : ''}`),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Relayed words retain their own clock; at remains the writer's clock. */
|
|
2
|
+
import { asArray, isObject, type Finding } from './check.ts';
|
|
3
|
+
import { latestPartsReceipt, latestPartsVerdictFinding, recordedPartsVerdict } from './parts-verdict-record.ts';
|
|
4
|
+
import { decodeReviewAct } from './review-act.ts';
|
|
5
|
+
import { readVerbatim } from './record-input.ts';
|
|
6
|
+
import { acceptedReviewVerdict, readVersions } from './review.ts';
|
|
7
|
+
import { collectReopens, liveSignoff } from './reopen.ts';
|
|
8
|
+
import { refuse, USAGE_EXIT } from './refusal.ts';
|
|
9
|
+
import type { JsonObject } from './templates.ts';
|
|
10
|
+
|
|
11
|
+
export const OWNER_CHAT_SOURCE = 'owner chat, relayed';
|
|
12
|
+
export const OWNER_CHAT_FLAGS = ['--owner-chat', '--said-at', '--relayed-by'];
|
|
13
|
+
const nonblank = (value: unknown): value is string => typeof value === 'string' && value.trim() !== '';
|
|
14
|
+
|
|
15
|
+
export function ownerChatFinding(record: JsonObject, path: string): Finding | undefined {
|
|
16
|
+
if (record.source !== OWNER_CHAT_SOURCE && record.said_at === undefined && record.relayed_by === undefined && record.owner_words === undefined) return;
|
|
17
|
+
const said = record.said_at;
|
|
18
|
+
if (record.source !== OWNER_CHAT_SOURCE || !nonblank(record.owner_words ?? record.verbatim)
|
|
19
|
+
|| !nonblank(record.by) || record.by.toLowerCase() === 'agent' || !nonblank(record.relayed_by)
|
|
20
|
+
|| typeof said !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(said)
|
|
21
|
+
|| !Number.isFinite(Date.parse(said)) || !Number.isFinite(Date.parse(String(record.at)))
|
|
22
|
+
|| Date.parse(said) > Date.parse(String(record.at)) || record.actId !== undefined || record.act_id !== undefined) {
|
|
23
|
+
return {code:'OWNER_CHAT_INVALID',path,message:`${path}: relayed owner chat needs exact nonblank words, owner identity, ISO said_at, relayed_by and a separate write at without an act id; record the owner's words with --owner-chat --said-at --relayed-by`};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readOwnerChat(opts: Record<string,string>, by: string, now: number, path: string): JsonObject | undefined {
|
|
28
|
+
if (!OWNER_CHAT_FLAGS.some(flag => opts[flag] !== undefined)) return;
|
|
29
|
+
if (OWNER_CHAT_FLAGS.some(flag => !nonblank(opts[flag]))) refuse('OWNER_CHAT_INVALID', `${path}: --owner-chat, --said-at and --relayed-by are required together; supply the words file and explicit owner clock and relay`, USAGE_EXIT);
|
|
30
|
+
if (opts['--from-review'] !== undefined || opts['--verbatim-file'] !== undefined || opts['--source'] !== undefined) refuse('OWNER_CHAT_INVALID', `${path}: owner chat excludes --from-review, --verbatim-file and --source; retain the relayed source`, USAGE_EXIT);
|
|
31
|
+
const record: JsonObject = {by, at:new Date(now).toISOString(), owner_words:readVerbatim(opts['--owner-chat']!),
|
|
32
|
+
said_at:opts['--said-at']!,relayed_by:opts['--relayed-by']!,source:OWNER_CHAT_SOURCE};
|
|
33
|
+
const finding = ownerChatFinding(record,path);
|
|
34
|
+
if (finding) refuse(finding.code,finding.message);
|
|
35
|
+
return record;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Select the current subject as next does, then require an accepted verdict still live at this review stage. */
|
|
39
|
+
export function ownerChatSignoffAllowed(dir: string, project: JsonObject, stage: string, draft?: string, version?: string): boolean {
|
|
40
|
+
const reopens = collectReopens(project,[]);
|
|
41
|
+
if (stage === 'parts-review') {
|
|
42
|
+
const current = asArray(project.drafts).filter(isObject).at(-1);
|
|
43
|
+
if (!current || typeof current.id !== 'string' || (draft !== undefined && draft !== current.id)) return false;
|
|
44
|
+
const receipt = latestPartsReceipt(project,current.id);
|
|
45
|
+
if (!receipt || latestPartsVerdictFinding(project,current.id,'signoffs.draft_id')) return false;
|
|
46
|
+
try {
|
|
47
|
+
const at = receipt.schema === 'rig-create/review-act@1' ? decodeReviewAct(String(receipt.bytes),'parts-verdict').at : receipt.at;
|
|
48
|
+
if (!Number.isFinite(Date.parse(String(at))) || !liveSignoff(reopens,'parts-review',Date.parse(String(at)))) return false;
|
|
49
|
+
recordedPartsVerdict(dir,project,current.id); return true;
|
|
50
|
+
} catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
if (stage !== 'rig-review' && stage !== 'direction-review') return true;
|
|
53
|
+
const phase = stage === 'rig-review' ? 'rigging' : 'post';
|
|
54
|
+
const current = asArray(project.versions).filter(isObject).filter(v => v.kind === (stage === 'rig-review' ? 'rig' : 'post')
|
|
55
|
+
&& (stage === 'rig-review' || liveSignoff(reopens,phase,Date.parse(String(v.at))))).at(-1);
|
|
56
|
+
if (!current || (version !== undefined && String(current.n) !== version)) return false;
|
|
57
|
+
if (stage === 'rig-review' && !readVersions(project).rigs.some(rig => rig.n === current.n && rig.live)) return false;
|
|
58
|
+
const index = acceptedReviewVerdict(current,stage);
|
|
59
|
+
const verdict = asArray(current.verdicts)[index];
|
|
60
|
+
return isObject(verdict) && liveSignoff(reopens,stage,Date.parse(String(verdict.at)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assertOwnerChatSignoff(dir: string, project: JsonObject, stage: string, draft?: string, version?: string): void {
|
|
64
|
+
if (ownerChatSignoffAllowed(dir,project,stage,draft,version)) return;
|
|
65
|
+
const route = stage === 'parts-review' ? '--draft <current-draft>' : '--rig <current-version>';
|
|
66
|
+
refuse('OWNER_CHAT_GATE_VISUAL', `project.json.signoffs.${stage}: an accepted live verdict for the current subject is required; get the verdict in the review tool first with rig-create job open ${route}, ingest it, then relay the owner's signoff`);
|
|
67
|
+
}
|
package/src/signoff.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OWNER_CHAT_FLAGS, readOwnerChat, assertOwnerChatSignoff } from './owner-chat.ts';
|
|
1
2
|
import { latestPartsVerdictFinding, recordedPartsVerdict } from './parts-verdict-record.ts';
|
|
2
3
|
import { selectAnswerHashes } from './signoff-answers.ts';
|
|
3
4
|
import { selectSignoffTarget } from './signoff-target.ts';
|
|
@@ -34,6 +35,9 @@ export const SIGNOFF_HELP = `signoff:
|
|
|
34
35
|
(excludes --by, --verbatim-file and --steps on both routes)
|
|
35
36
|
--by <who> the person signing (agent is refused)
|
|
36
37
|
--verbatim-file <file> UTF-8 words unchanged, or - for stdin (default: stdin)
|
|
38
|
+
--owner-chat <file> relay exact owner words (excludes --verbatim-file / --from-review)
|
|
39
|
+
--said-at <ISO date> explicit owner clock; at remains the write time
|
|
40
|
+
--relayed-by <who> operator relaying the owner chat
|
|
37
41
|
--steps <id,id,...> narrow parts plan, rigging-interview rig steps, or post steps
|
|
38
42
|
--checkpoint <id> sign an enabled checkpoint in this execution stage
|
|
39
43
|
--from-review <actId> with --checkpoint: record the passed Review checkpoint act (no --by or --verbatim-file)
|
|
@@ -75,7 +79,7 @@ export function signoffCommand(args: string[], now: number, hooks: SignoffHooks
|
|
|
75
79
|
i++;
|
|
76
80
|
} else if (arg.startsWith('--component=') || arg.startsWith('--diff-report=') || arg.startsWith('--triage-file=')) {
|
|
77
81
|
hasDiffFlag = true;
|
|
78
|
-
} else if (['--version', '--draft', '--structure-file', '--by', '--verbatim-file', '--steps', '--checkpoint', '--from-review'].includes(arg)) {
|
|
82
|
+
} else if (['--version', '--draft', '--structure-file', '--by', '--verbatim-file', '--steps', '--checkpoint', '--from-review', ...OWNER_CHAT_FLAGS].includes(arg)) {
|
|
79
83
|
const value = args[++i];
|
|
80
84
|
if (arg in opts || value === undefined || value === '') refuse('SIGNOFF_OPTION_INVALID', `${arg} needs one non-empty value`, USAGE_EXIT);
|
|
81
85
|
opts[arg] = value;
|
|
@@ -94,7 +98,7 @@ export function signoffCommand(args: string[], now: number, hooks: SignoffHooks
|
|
|
94
98
|
}
|
|
95
99
|
const actId = opts['--from-review'];
|
|
96
100
|
if (actId !== undefined) {
|
|
97
|
-
const mixed = ['--by', '--verbatim-file', '--steps'].filter(flag => opts[flag] !== undefined);
|
|
101
|
+
const mixed = ['--by', '--verbatim-file', '--steps', ...OWNER_CHAT_FLAGS].filter(flag => opts[flag] !== undefined);
|
|
98
102
|
if (mixed.length > 0) refuse('SIGNOFF_OPTION_INVALID', `project.json.review_acts: --from-review excludes ${mixed.join(', ')}; the person's identity, words and covered steps come from the review act`, USAGE_EXIT);
|
|
99
103
|
}
|
|
100
104
|
if (actId === undefined) return recordSignoff(stage, positional, opts, json, now, hooks);
|
|
@@ -114,7 +118,9 @@ function recordSignoff(stage: string, positional: string[], opts: Record<string,
|
|
|
114
118
|
if (opts['--by'] === undefined && actId === undefined) refuse('SIGNOFF_OPTION_INVALID', 'signoff needs --by', USAGE_EXIT);
|
|
115
119
|
if (by.toLowerCase() === 'agent') refuse('SIGNOFF_NOT_HUMAN', `stage ${stage}: signed by agent`);
|
|
116
120
|
const dir = resolve(positional[1] ?? process.cwd());
|
|
121
|
+
const chat = readOwnerChat(opts, by, now, 'project.json.signoffs');
|
|
117
122
|
const { file, project } = readProject(dir);
|
|
123
|
+
if (chat) assertOwnerChatSignoff(dir, project, stage, opts['--draft'], opts['--version']);
|
|
118
124
|
const original = actId === undefined ? undefined : readFileSync(file);
|
|
119
125
|
let ingest: CheckpointIngest | undefined;
|
|
120
126
|
// Recorded checkpoint receipts replay before current eligibility, through the same immutable-byte validator.
|
|
@@ -224,7 +230,7 @@ function recordSignoff(stage: string, positional: string[], opts: Record<string,
|
|
|
224
230
|
: checkpoint.state === 'delegated' && checkpoint.decided && isObject(checkpoint.answer.agent_decision) ? checkpoint.answer.agent_decision.value : undefined;
|
|
225
231
|
if (typeof value !== 'boolean') refuse('SIGNOFF_CHECKPOINT_INVALID', `${String(step.id)}: checkpoint needs a boolean user answer or a decided boolean delegation`);
|
|
226
232
|
}
|
|
227
|
-
const verbatim = review !== undefined ? String(review.human.verbatim) : ingest ? ingest.verbatim : readVerbatim(opts['--verbatim-file'] ?? '-');
|
|
233
|
+
const verbatim = review !== undefined ? String(review.human.verbatim) : ingest ? ingest.verbatim : chat ? String(chat.owner_words) : readVerbatim(opts['--verbatim-file'] ?? '-');
|
|
228
234
|
const at = review !== undefined ? String(review.human.at) : ingest ? ingest.at : new Date(now).toISOString();
|
|
229
235
|
const hashes: JsonObject = Object.create(null) as JsonObject;
|
|
230
236
|
for (const step of covered) {
|
|
@@ -234,6 +240,7 @@ function recordSignoff(stage: string, positional: string[], opts: Record<string,
|
|
|
234
240
|
hashes[step.id as string] = stepHash(step);
|
|
235
241
|
}
|
|
236
242
|
const signoff: JsonObject = { stage, checkpoint: checkpointId ?? null, by, at, verbatim };
|
|
243
|
+
if (chat) { signoff.source = chat.source!; signoff.said_at = chat.said_at!; signoff.relayed_by = chat.relayed_by!; }
|
|
237
244
|
if (review !== undefined) signoff.source = review.human.source!;
|
|
238
245
|
if (evidence !== undefined) signoff.evidence = evidence;
|
|
239
246
|
if (phase !== undefined) {
|
package/templates/agent-guide.md
CHANGED
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per {{RESUME_LINK}}.
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ One painting; one rig and its takes.
|
|
|
4
4
|
|
|
5
5
|
## Start here
|
|
6
6
|
|
|
7
|
-
1. Read `project.json`.
|
|
7
|
+
1. Read `project.json`. Verdicts go through Review; interview picks and signoffs may relay owner words via `--owner-chat --said-at --relayed-by`. Never invent human words.
|
|
8
8
|
2. Run `rig-create next` to derive the next moves and whose they are.
|
|
9
9
|
3. Resume per [Resume](https://github.com/rig-c/create/blob/main/docs/PROJECT.md#how-an-agent-resumes-a-project).
|
|
10
10
|
|