@quolu/lattice 0.61.3 → 0.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +6 -6
- package/bin/lattice.mjs +20 -9
- package/package.json +1 -1
- package/src/cli-help.mjs +9 -5
- package/src/project-cli.mjs +22 -71
- package/src/runtime-pull-intake.mjs +11 -5
- package/src/todo-authoring-input.mjs +183 -0
- package/src/todo-cli.mjs +367 -382
- package/src/todo-independence-contracts.mjs +11 -4
- package/src/todo-independence-guidance.mjs +4 -4
- package/src/todo-store.mjs +6 -30
- package/src/witness-scaffold.mjs +11 -4
package/src/todo-cli.mjs
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { gitSync } from './git-process.mjs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
hostname,
|
|
5
|
+
} from 'node:os';
|
|
3
6
|
import {
|
|
4
7
|
lstat, mkdir, readFile, realpath, writeFile,
|
|
5
8
|
} from 'node:fs/promises';
|
|
6
9
|
import path from 'node:path';
|
|
7
10
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
isAuthoringPathToken,
|
|
13
|
+
matchFlagCommand,
|
|
14
|
+
parseAuthoringJson,
|
|
15
|
+
readAuthoringJsonFile,
|
|
16
|
+
resolveAuthoringInputPath,
|
|
17
|
+
} from './todo-authoring-input.mjs';
|
|
9
18
|
|
|
10
19
|
import {
|
|
11
20
|
TODO_COORDINATION_MODES,
|
|
@@ -169,7 +178,6 @@ import { commitTodoStoreMutation } from './todo-store-git-transaction.mjs';
|
|
|
169
178
|
|
|
170
179
|
const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
|
|
171
180
|
const DEFAULT_GANTT_SCOPE = 'live';
|
|
172
|
-
const MAX_MIGRATION_INPUT_BYTES = 8_388_608;
|
|
173
181
|
const MAX_NOTE_INPUT_BYTES = 16_384;
|
|
174
182
|
const ACTOR_ENV_KEYS = Object.freeze([
|
|
175
183
|
'LATTICE_TODO_ACTOR_HOST',
|
|
@@ -315,25 +323,11 @@ function selectNoteTask(member, requestedTaskId) {
|
|
|
315
323
|
}
|
|
316
324
|
|
|
317
325
|
async function readNoteTextInput(repoRoot, inputRef) {
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo');
|
|
323
|
-
}
|
|
324
|
-
let metadata;
|
|
325
|
-
try { metadata = await lstat(absolute); } catch {
|
|
326
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_missing');
|
|
327
|
-
}
|
|
328
|
-
if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > MAX_NOTE_INPUT_BYTES) {
|
|
329
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'unsafe_or_oversized_note_input');
|
|
330
|
-
}
|
|
331
|
-
const resolved = await realpath(absolute);
|
|
332
|
-
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
333
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_alias_or_escape');
|
|
326
|
+
const located = await resolveAuthoringInputPath(repoRoot, inputRef);
|
|
327
|
+
const bytes = await readFile(located.absolute);
|
|
328
|
+
if (bytes.length > MAX_NOTE_INPUT_BYTES) {
|
|
329
|
+
throw new TodoStoreError('INPUT_TOO_LARGE', 'note_input_too_large');
|
|
334
330
|
}
|
|
335
|
-
const bytes = await readFile(resolved);
|
|
336
|
-
if (bytes.length > MAX_NOTE_INPUT_BYTES) throw new TodoStoreError('INPUT_TOO_LARGE', 'note_input_too_large');
|
|
337
331
|
try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); }
|
|
338
332
|
catch { throw new TodoStoreError('INPUT_UNREADABLE', 'note_input_invalid_utf8'); }
|
|
339
333
|
}
|
|
@@ -368,58 +362,8 @@ function within(root, candidate) {
|
|
|
368
362
|
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
369
363
|
}
|
|
370
364
|
|
|
371
|
-
function hasDuplicateJsonKey(node) {
|
|
372
|
-
if (node?.type === 'object') {
|
|
373
|
-
const keys = new Set();
|
|
374
|
-
for (const property of node.children ?? []) {
|
|
375
|
-
const [key, value] = property.children ?? [];
|
|
376
|
-
if (keys.has(key?.value) || hasDuplicateJsonKey(value)) return true;
|
|
377
|
-
keys.add(key?.value);
|
|
378
|
-
}
|
|
379
|
-
} else if (node?.type === 'array') {
|
|
380
|
-
return (node.children ?? []).some(hasDuplicateJsonKey);
|
|
381
|
-
}
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
365
|
async function readMigrationInput(repoRoot, inputRef, { requireValid = true } = {}) {
|
|
386
|
-
const
|
|
387
|
-
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
388
|
-
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
389
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined, { input_ref: inputRef });
|
|
390
|
-
}
|
|
391
|
-
let stats;
|
|
392
|
-
try { stats = await lstat(absolute); } catch {
|
|
393
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_missing', undefined, { input_ref: inputRef });
|
|
394
|
-
}
|
|
395
|
-
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
396
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'unsafe_input_path', undefined, { input_ref: inputRef });
|
|
397
|
-
}
|
|
398
|
-
const resolved = await realpath(absolute);
|
|
399
|
-
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
400
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_alias_or_escape', undefined, { input_ref: inputRef });
|
|
401
|
-
}
|
|
402
|
-
if (stats.size > MAX_MIGRATION_INPUT_BYTES) {
|
|
403
|
-
throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
404
|
-
}
|
|
405
|
-
const bytes = await readFile(resolved);
|
|
406
|
-
if (bytes.length > MAX_MIGRATION_INPUT_BYTES) {
|
|
407
|
-
throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
408
|
-
}
|
|
409
|
-
let text;
|
|
410
|
-
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch {
|
|
411
|
-
throw new TodoStoreError('INVALID_JSON', 'invalid_utf8');
|
|
412
|
-
}
|
|
413
|
-
const parseErrors = [];
|
|
414
|
-
const tree = parseTree(text, parseErrors, { allowTrailingComma: false, disallowComments: true });
|
|
415
|
-
if (parseErrors.length > 0 || tree === undefined) {
|
|
416
|
-
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
417
|
-
}
|
|
418
|
-
if (hasDuplicateJsonKey(tree)) throw new TodoStoreError('INVALID_JSON', 'duplicate_key');
|
|
419
|
-
let extraction;
|
|
420
|
-
try { extraction = JSON.parse(text); } catch {
|
|
421
|
-
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
422
|
-
}
|
|
366
|
+
const extraction = await readAuthoringJsonFile(repoRoot, inputRef);
|
|
423
367
|
if (!requireValid) return extraction;
|
|
424
368
|
if (![TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(extraction?.schema)) {
|
|
425
369
|
throw new TodoStoreError('INVALID_TODO_EXTRACTION', 'todo_extraction_schema_unsupported', undefined, {
|
|
@@ -454,54 +398,14 @@ async function readRevisionInput(repoRoot, inputRef, {
|
|
|
454
398
|
invalidReason = 'revision_schema_or_digest_invalid',
|
|
455
399
|
explain = explainTodoRevision,
|
|
456
400
|
} = {}) {
|
|
457
|
-
const
|
|
458
|
-
const absolute = path.resolve(canonicalRoot, inputRef);
|
|
459
|
-
if (!within(canonicalRoot, absolute) || absolute === canonicalRoot) {
|
|
460
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_outside_repo', undefined, { input_ref: inputRef });
|
|
461
|
-
}
|
|
462
|
-
let stats;
|
|
463
|
-
try { stats = await lstat(absolute); } catch {
|
|
464
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_missing', undefined, { input_ref: inputRef });
|
|
465
|
-
}
|
|
466
|
-
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
467
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'unsafe_input_path', undefined, { input_ref: inputRef });
|
|
468
|
-
}
|
|
469
|
-
const resolved = await realpath(absolute);
|
|
470
|
-
if (resolved !== absolute || !within(canonicalRoot, resolved)) {
|
|
471
|
-
throw new TodoStoreError('INPUT_UNREADABLE', 'input_path_alias_or_escape', undefined, { input_ref: inputRef });
|
|
472
|
-
}
|
|
473
|
-
if (stats.size > MAX_MIGRATION_INPUT_BYTES) throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
474
|
-
const bytes = await readFile(resolved);
|
|
475
|
-
if (bytes.length > MAX_MIGRATION_INPUT_BYTES) throw new TodoStoreError('INPUT_TOO_LARGE', 'input_size_limit_exceeded');
|
|
476
|
-
let text;
|
|
477
|
-
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch {
|
|
478
|
-
throw new TodoStoreError('INVALID_JSON', 'invalid_utf8');
|
|
479
|
-
}
|
|
480
|
-
if (!text.endsWith('\n') || text.startsWith('\uFEFF') || text.includes('\r')
|
|
481
|
-
|| text.slice(0, -1).includes('\n')) {
|
|
482
|
-
throw new TodoStoreError(invalidCode, 'non_canonical_revision_bytes');
|
|
483
|
-
}
|
|
484
|
-
const parseErrors = [];
|
|
485
|
-
const tree = parseTree(text.slice(0, -1), parseErrors, { allowTrailingComma: false, disallowComments: true });
|
|
486
|
-
if (parseErrors.length > 0 || tree === undefined) throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
487
|
-
if (hasDuplicateJsonKey(tree)) throw new TodoStoreError('INVALID_JSON', 'duplicate_key');
|
|
488
|
-
let revision;
|
|
489
|
-
try { revision = JSON.parse(text.slice(0, -1)); } catch {
|
|
490
|
-
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
|
|
491
|
-
}
|
|
401
|
+
const revision = await readAuthoringJsonFile(repoRoot, inputRef, { invalidCode });
|
|
492
402
|
if (!validate(revision)) {
|
|
493
|
-
// 「schema_or_digest_invalid」だけでは何のfieldがどう壊れているか分からない
|
|
494
|
-
// (ADR 0130の案内規律)。explainは可否判定を変えず、診断だけを追加する。
|
|
495
|
-
// 呼び出し元がexplainを渡さない(phase decision入力等)場合はdetail無しのまま。
|
|
496
403
|
const explained = explain === null ? null : explain(revision);
|
|
497
404
|
throw new TodoStoreError(invalidCode, invalidReason, undefined,
|
|
498
405
|
explained === null || explained.valid ? undefined : {
|
|
499
406
|
violation_reason: explained.reason, violation_path: explained.path,
|
|
500
407
|
});
|
|
501
408
|
}
|
|
502
|
-
if (text !== `${canonicalizeTodoArtifact(revision)}\n`) {
|
|
503
|
-
throw new TodoStoreError(invalidCode, 'non_canonical_revision_bytes');
|
|
504
|
-
}
|
|
505
409
|
return revision;
|
|
506
410
|
}
|
|
507
411
|
|
|
@@ -516,63 +420,97 @@ const EVIDENCE_DESCRIPTOR_EXPECTED = Object.freeze({
|
|
|
516
420
|
+ 'content_digestはblob bytesのsha256(hex)で得る。refsから到達可能なblobだけが検証を通る',
|
|
517
421
|
});
|
|
518
422
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
expected: EVIDENCE_DESCRIPTOR_EXPECTED,
|
|
523
|
-
});
|
|
423
|
+
function evidenceIdFor(planKey, taskId) {
|
|
424
|
+
const compact = `ev-${planKey}-${taskId}`.replace(/[^0-9A-Za-z._-]/gu, '-').slice(0, 128);
|
|
425
|
+
return isTodoIdentifier(compact) ? compact : 'evidence';
|
|
524
426
|
}
|
|
525
427
|
|
|
526
|
-
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
428
|
+
function mediaTypeFor(ref) {
|
|
429
|
+
if (ref.endsWith('.md')) return 'text/markdown';
|
|
430
|
+
if (ref.endsWith('.json')) return 'application/json';
|
|
431
|
+
return 'text/plain';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function looksLikeEvidenceDescriptor(value) {
|
|
435
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function writeEvidenceBlob(repoRoot, bytes) {
|
|
439
|
+
const oid = gitSync(['hash-object', '-w', '--stdin'], {
|
|
440
|
+
cwd: repoRoot, input: bytes, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'],
|
|
441
|
+
}).trim();
|
|
442
|
+
return {
|
|
443
|
+
git_blob_oid: oid,
|
|
444
|
+
content_digest: createHash('sha256').update(bytes).digest('hex'),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function resolveDoneEvidence({ repoRoot, evidenceRef, evidenceMessage, planKey, taskId }) {
|
|
449
|
+
if (typeof evidenceMessage === 'string') {
|
|
450
|
+
const bytes = Buffer.from(evidenceMessage, 'utf8');
|
|
451
|
+
const descriptor = {
|
|
452
|
+
evidence_id: evidenceIdFor(planKey, taskId),
|
|
453
|
+
repo_id: 'self',
|
|
454
|
+
path: `.lattice/todo/evidence/${planKey}/${taskId}.md`,
|
|
455
|
+
...writeEvidenceBlob(repoRoot, bytes),
|
|
456
|
+
media_type: 'text/markdown',
|
|
457
|
+
anchor_digest: null,
|
|
458
|
+
};
|
|
459
|
+
if (!validateEvidenceDescriptor(descriptor) || !isTodoRef(descriptor.path)) {
|
|
460
|
+
throw new TodoStoreError('INVALID_EVIDENCE', 'authored_evidence_descriptor_invalid');
|
|
461
|
+
}
|
|
462
|
+
return descriptor;
|
|
552
463
|
}
|
|
464
|
+
const located = await resolveAuthoringInputPath(repoRoot, evidenceRef);
|
|
465
|
+
const bytes = await readFile(located.absolute);
|
|
553
466
|
let text;
|
|
554
|
-
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); }
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
467
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); }
|
|
468
|
+
catch { throw new TodoStoreError('INVALID_EVIDENCE', 'evidence_input_invalid_utf8'); }
|
|
469
|
+
let parsed = null;
|
|
470
|
+
try {
|
|
471
|
+
parsed = parseAuthoringJson(text, { invalidCode: 'INVALID_JSON' });
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error instanceof TodoStoreError && error.code !== 'INVALID_JSON') throw error;
|
|
559
474
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
475
|
+
if (looksLikeEvidenceDescriptor(parsed)) {
|
|
476
|
+
if (!validateEvidenceDescriptor(parsed)) {
|
|
477
|
+
throw new TodoStoreError('INVALID_EVIDENCE', 'schema_invalid', undefined, {
|
|
478
|
+
expected: EVIDENCE_DESCRIPTOR_EXPECTED,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return parsed;
|
|
482
|
+
}
|
|
483
|
+
const descriptor = {
|
|
484
|
+
evidence_id: evidenceIdFor(planKey, taskId),
|
|
485
|
+
repo_id: 'self',
|
|
486
|
+
path: located.inputRef,
|
|
487
|
+
...writeEvidenceBlob(repoRoot, bytes),
|
|
488
|
+
media_type: mediaTypeFor(located.inputRef),
|
|
489
|
+
anchor_digest: null,
|
|
490
|
+
};
|
|
491
|
+
if (!validateEvidenceDescriptor(descriptor)) {
|
|
492
|
+
throw new TodoStoreError('INVALID_EVIDENCE', 'authored_evidence_descriptor_invalid');
|
|
567
493
|
}
|
|
568
|
-
|
|
494
|
+
return descriptor;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function readEvidenceInput(repoRoot, inputRef) {
|
|
498
|
+
return resolveDoneEvidence({
|
|
499
|
+
repoRoot, evidenceRef: inputRef, evidenceMessage: null, planKey: 'evidence', taskId: 'body',
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function readJsonInput(repoRoot, inputRef, { validate, invalidCode, expected = null }) {
|
|
569
504
|
let descriptor;
|
|
570
|
-
try {
|
|
571
|
-
|
|
505
|
+
try {
|
|
506
|
+
descriptor = await readAuthoringJsonFile(repoRoot, inputRef, { invalidCode: 'INVALID_JSON' });
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error?.code === 'INVALID_JSON' && expected !== null && error.detail?.reason === 'json_parse_failed') {
|
|
509
|
+
throw new TodoStoreError('INVALID_JSON', 'json_parse_failed', undefined, { expected });
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
572
512
|
}
|
|
573
513
|
if (!validate(descriptor)) {
|
|
574
|
-
// 「schema_invalid」だけを返すと、呼び出したAIは何をどう直せばよいか分からない。
|
|
575
|
-
// 期待する形を渡されている入口は、それをそのまま返す(ADR 0130の案内規律)。
|
|
576
514
|
throw new TodoStoreError(invalidCode, 'schema_invalid', undefined,
|
|
577
515
|
expected === null ? undefined : { expected });
|
|
578
516
|
}
|
|
@@ -607,23 +545,35 @@ async function readStructureSetDraft(repoRoot, inputRef) {
|
|
|
607
545
|
});
|
|
608
546
|
}
|
|
609
547
|
|
|
548
|
+
function sanitizeActorIdentifier(raw, fallback) {
|
|
549
|
+
const compact = String(raw ?? '').replace(/[^0-9A-Za-z._-]/gu, '-').replace(/^-+/u, '').slice(0, 128);
|
|
550
|
+
return isTodoIdentifier(compact) ? compact : fallback;
|
|
551
|
+
}
|
|
552
|
+
|
|
610
553
|
function mutationActor(env) {
|
|
611
|
-
const
|
|
612
|
-
const
|
|
613
|
-
.filter(({ value }) => typeof value !== 'string' || value.length === 0)
|
|
614
|
-
.map(({ key }) => key);
|
|
615
|
-
const invalidEnvironment = entries
|
|
554
|
+
const provided = ACTOR_ENV_KEYS.map((key) => ({ key, value: env[key] }));
|
|
555
|
+
const invalidEnvironment = provided
|
|
616
556
|
.filter(({ value }) => typeof value === 'string' && value.length > 0 && !isTodoIdentifier(value))
|
|
617
557
|
.map(({ key }) => key);
|
|
618
|
-
if (
|
|
558
|
+
if (invalidEnvironment.length > 0) {
|
|
619
559
|
throw new TodoStoreError('ACTOR_UNRESOLVED', 'actor_environment_invalid', undefined, {
|
|
620
560
|
required_environment: ACTOR_ENV_KEYS,
|
|
621
|
-
missing_environment:
|
|
561
|
+
missing_environment: [],
|
|
622
562
|
invalid_environment: invalidEnvironment,
|
|
623
563
|
next_action: 'set_required_actor_environment_and_retry',
|
|
624
564
|
});
|
|
625
565
|
}
|
|
626
|
-
return {
|
|
566
|
+
return {
|
|
567
|
+
host: isTodoIdentifier(env.LATTICE_TODO_ACTOR_HOST)
|
|
568
|
+
? env.LATTICE_TODO_ACTOR_HOST
|
|
569
|
+
: sanitizeActorIdentifier(hostname(), 'host'),
|
|
570
|
+
session: isTodoIdentifier(env.LATTICE_TODO_ACTOR_SESSION)
|
|
571
|
+
? env.LATTICE_TODO_ACTOR_SESSION
|
|
572
|
+
: 'session',
|
|
573
|
+
agent: isTodoIdentifier(env.LATTICE_TODO_ACTOR_AGENT)
|
|
574
|
+
? env.LATTICE_TODO_ACTOR_AGENT
|
|
575
|
+
: sanitizeActorIdentifier(env.USER ?? env.LOGNAME, 'agent'),
|
|
576
|
+
};
|
|
627
577
|
}
|
|
628
578
|
|
|
629
579
|
/**
|
|
@@ -664,11 +614,15 @@ function terminalAuditDoneAdvisory(plan, phases) {
|
|
|
664
614
|
}
|
|
665
615
|
|
|
666
616
|
async function mutate({
|
|
667
|
-
repoRoot, env, planKey, taskId, kind, payload, evidenceRef,
|
|
668
|
-
noteContext = null, structureContext = null, testResultRef = null,
|
|
617
|
+
repoRoot, env, planKey, taskId, kind, payload, evidenceRef, evidenceMessage = null,
|
|
618
|
+
advisory = null, noteContext = null, structureContext = null, testResultRef = null,
|
|
669
619
|
}) {
|
|
670
620
|
const actor = mutationActor(env);
|
|
671
|
-
const evidence = evidenceRef === null
|
|
621
|
+
const evidence = evidenceRef === null && evidenceMessage === null
|
|
622
|
+
? null
|
|
623
|
+
: await resolveDoneEvidence({
|
|
624
|
+
repoRoot, evidenceRef, evidenceMessage, planKey, taskId,
|
|
625
|
+
});
|
|
672
626
|
const testResult = testResultRef === null ? null : await readTestResultInput(repoRoot, testResultRef);
|
|
673
627
|
let eventPayload = payload;
|
|
674
628
|
if (kind === 'done' && payload === 'authored') {
|
|
@@ -731,13 +685,23 @@ async function startStructureContext({ repoRoot, store, planKey, taskId }) {
|
|
|
731
685
|
structure_set_digest: null, task: null, next_actions: [],
|
|
732
686
|
};
|
|
733
687
|
if (source.structure_set_digest !== binding.structure_set_digest) {
|
|
734
|
-
|
|
735
|
-
'
|
|
688
|
+
return {
|
|
689
|
+
status: 'unreadable', enabled: true, freshness: 'stale',
|
|
690
|
+
stale_reasons: ['enabled_structure_source_unreadable'],
|
|
691
|
+
structure_set_digest: source.structure_set_digest,
|
|
692
|
+
task: null,
|
|
693
|
+
next_actions: [`lattice todo structure compile --plan ${planKey} --input <file>`],
|
|
694
|
+
};
|
|
736
695
|
}
|
|
737
696
|
const task = source.tasks.find(({ task_id: id }) => id === taskId);
|
|
738
697
|
if (task === undefined) {
|
|
739
|
-
|
|
740
|
-
'
|
|
698
|
+
return {
|
|
699
|
+
status: 'unreadable', enabled: true, freshness: 'stale',
|
|
700
|
+
stale_reasons: ['enabled_structure_task_missing'],
|
|
701
|
+
structure_set_digest: source.structure_set_digest,
|
|
702
|
+
task: null,
|
|
703
|
+
next_actions: [`lattice todo structure compile --plan ${planKey} --input <file>`],
|
|
704
|
+
};
|
|
741
705
|
}
|
|
742
706
|
const state = await readTodoStructureState({ repoRoot, store, planKey });
|
|
743
707
|
return {
|
|
@@ -873,10 +837,26 @@ async function startTask({
|
|
|
873
837
|
});
|
|
874
838
|
}
|
|
875
839
|
const resolvedTaskId = readyTask?.task_id ?? taskMatches[0].task_id;
|
|
876
|
-
// noteはjournal append
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
840
|
+
// noteはjournal appendより前に読む。壊れていてもstartは通す(ADR 0166 / 0181)。
|
|
841
|
+
let noteContext;
|
|
842
|
+
try {
|
|
843
|
+
({ context: noteContext } = await readTodoNoteContext({
|
|
844
|
+
repoRoot, store, planKey, taskId: resolvedTaskId,
|
|
845
|
+
}));
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (!['NOTE_LOG_CORRUPT', 'NOTE_PROJECTION_INVALID'].includes(error?.code)) {
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
noteContext = {
|
|
851
|
+
schema: 'lattice.todo_note_context.unreadable.v1',
|
|
852
|
+
project_id: member.plan.project_id,
|
|
853
|
+
plan_key: planKey,
|
|
854
|
+
task_id: resolvedTaskId,
|
|
855
|
+
code: error.code,
|
|
856
|
+
reason: error.detail?.reason ?? 'note_context_unreadable',
|
|
857
|
+
next_action: `lattice todo note list --plan ${planKey} --json`,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
880
860
|
// 助言はjournalへ書く前に確定させる。計算できないならstart自体を止める。
|
|
881
861
|
const advisory = await startAdvisory({
|
|
882
862
|
repoRoot, store, projection, planKey, taskId: resolvedTaskId,
|
|
@@ -1788,25 +1768,6 @@ function changedPathsSince(repoRoot, baseSha) {
|
|
|
1788
1768
|
.sort();
|
|
1789
1769
|
}
|
|
1790
1770
|
|
|
1791
|
-
function requireCleanWorktree(repoRoot) {
|
|
1792
|
-
let porcelain;
|
|
1793
|
-
try {
|
|
1794
|
-
porcelain = gitSync(['status', '--porcelain'], {
|
|
1795
|
-
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
1796
|
-
});
|
|
1797
|
-
} catch {
|
|
1798
|
-
throw new TodoStoreError('INDEPENDENCE_BASE_UNRESOLVED', 'git_status_unresolved');
|
|
1799
|
-
}
|
|
1800
|
-
const dirty = porcelain.split('\n').filter((line) => line.trim().length > 0);
|
|
1801
|
-
if (dirty.length > 0) {
|
|
1802
|
-
// 未commitの観測を検証済み証拠として固定化しない(ADR 0127 Decision 3)。
|
|
1803
|
-
throw new TodoStoreError('INDEPENDENCE_WORKTREE_DIRTY', 'worktree_not_clean', undefined, {
|
|
1804
|
-
changed_entries: dirty.length,
|
|
1805
|
-
next_action: 'commit_or_stash_then_retry',
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
1771
|
function structureGitIdentity(repoRoot, baselineSha, violations) {
|
|
1811
1772
|
let currentHeadSha = null;
|
|
1812
1773
|
try {
|
|
@@ -1988,7 +1949,7 @@ function parseAutomatedStructureRealizeArgs(argv) {
|
|
|
1988
1949
|
let realizedRef = null;
|
|
1989
1950
|
if (argv[6] === '--planned') {
|
|
1990
1951
|
cursor = 7; usePlanned = true;
|
|
1991
|
-
} else if (argv[6] === '--realized' &&
|
|
1952
|
+
} else if (argv[6] === '--realized' && isAuthoringPathToken(argv[7])) {
|
|
1992
1953
|
cursor = 8; usePlanned = false; realizedRef = argv[7];
|
|
1993
1954
|
} else {
|
|
1994
1955
|
return null;
|
|
@@ -2656,7 +2617,6 @@ async function seamProfile({ repoRoot, planKey, filePath }) {
|
|
|
2656
2617
|
}
|
|
2657
2618
|
|
|
2658
2619
|
async function seamProposalCompile({ repoRoot, planKey }) {
|
|
2659
|
-
requireCleanWorktree(repoRoot);
|
|
2660
2620
|
const currentBaseSha = currentHeadSha(repoRoot);
|
|
2661
2621
|
const store = await readTodoStore({ repoRoot });
|
|
2662
2622
|
const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
|
|
@@ -3421,26 +3381,22 @@ function writesTodoStore(argv) {
|
|
|
3421
3381
|
|
|
3422
3382
|
async function ensureActiveProjectDashboard({ repoRoot, env }) {
|
|
3423
3383
|
if (env.LATTICE_DASHBOARD_AUTOSTART === '0') return null;
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
const sessionId =
|
|
3427
|
-
|
|
3384
|
+
let actor;
|
|
3385
|
+
try { actor = mutationActor(env); } catch { return null; }
|
|
3386
|
+
const sessionId = actor.session;
|
|
3387
|
+
let store;
|
|
3388
|
+
try { store = await readTodoStoreStable({ repoRoot }); }
|
|
3389
|
+
catch { return null; }
|
|
3428
3390
|
let identity;
|
|
3429
|
-
try { identity = await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env }); }
|
|
3430
|
-
|
|
3431
|
-
'project_identity_resolve_failed', undefined, error?.detail ?? {});
|
|
3432
|
-
}
|
|
3391
|
+
try { identity = await resolveProjectIdentity({ repoRoot, projectId: store.project_id, env }); }
|
|
3392
|
+
catch { return null; }
|
|
3433
3393
|
try {
|
|
3434
3394
|
return await ensureTodoDashboardActivity({
|
|
3435
3395
|
repoRoot, projectId: store.project_id, displayName: identity.displayName, sessionId, env,
|
|
3436
3396
|
});
|
|
3437
|
-
} catch
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
project_id: store.project_id,
|
|
3441
|
-
...(typeof error?.detail?.next_action === 'string'
|
|
3442
|
-
? { next_action: error.detail.next_action } : {}),
|
|
3443
|
-
});
|
|
3397
|
+
} catch {
|
|
3398
|
+
// dashboardは副作用。start/doneをdashboard故障で止めない(ADR 0181)。
|
|
3399
|
+
return null;
|
|
3444
3400
|
}
|
|
3445
3401
|
}
|
|
3446
3402
|
|
|
@@ -3578,38 +3534,6 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3578
3534
|
return atomicStoreCommitUnsupported(stderr, argv);
|
|
3579
3535
|
}
|
|
3580
3536
|
|
|
3581
|
-
if (argv[0] === 'migrate' && argv[1] === '--input'
|
|
3582
|
-
&& typeof argv[2] === 'string' && path.isAbsolute(argv[2])) {
|
|
3583
|
-
return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
|
|
3584
|
-
argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
|
|
3585
|
-
next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
|
|
3586
|
-
});
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
if (argv[0] === 'structure' && ['input', 'compile'].includes(argv[1])
|
|
3590
|
-
&& argv[4] === '--input' && typeof argv[5] === 'string' && path.isAbsolute(argv[5])) {
|
|
3591
|
-
return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
|
|
3592
|
-
argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
|
|
3593
|
-
next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
|
|
3594
|
-
});
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
|
-
if (argv[0] === 'structure' && argv[1] === 'realize'
|
|
3598
|
-
&& argv[6] === '--input' && typeof argv[7] === 'string' && path.isAbsolute(argv[7])) {
|
|
3599
|
-
return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
|
|
3600
|
-
argument: '--input', expected: 'repo-relative path', actual: 'absolute path',
|
|
3601
|
-
next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
|
|
3602
|
-
});
|
|
3603
|
-
}
|
|
3604
|
-
|
|
3605
|
-
if (argv[0] === 'structure' && argv[1] === 'realize'
|
|
3606
|
-
&& argv[6] === '--realized' && typeof argv[7] === 'string' && path.isAbsolute(argv[7])) {
|
|
3607
|
-
return typedArgumentFailure(stderr, 'INPUT_OUTSIDE_REPOSITORY', 'absolute_input_path_rejected', {
|
|
3608
|
-
argument: '--realized', expected: 'repo-relative path', actual: 'absolute path',
|
|
3609
|
-
next_action: 'place_the_input_inside_the_repository_and_pass_a_repo_relative_path',
|
|
3610
|
-
});
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
3537
|
// `--schema --json`はstoreを読まない決定的な出力(`plan create --schema`と同じ規律)。
|
|
3614
3538
|
// 通常dispatchより前に処理し、repoRoot解決やdashboard daemon起動を経由させない。
|
|
3615
3539
|
if (argv.length === 3 && argv[1] === '--schema' && argv[2] === '--json'
|
|
@@ -3663,33 +3587,25 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3663
3587
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
3664
3588
|
&& (argv.length === 3 || argv[3] === '--json')) {
|
|
3665
3589
|
action = (repoRoot) => bindings({ repoRoot, requestedPlanKey: argv[2] });
|
|
3666
|
-
} else if (
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
// `--task`省略でplan単位note。工程レベルの義務(順序制約・一度きりの観測が在ること)は
|
|
3686
|
-
// 特定のtaskに属さない。
|
|
3687
|
-
action = (repoRoot) => appendNote({
|
|
3688
|
-
repoRoot, env, planKey: argv[2], taskId: null,
|
|
3689
|
-
message: argv[3] === '--message' ? argv[4] : null,
|
|
3690
|
-
inputRef: argv[3] === '--input' ? argv[4] : null,
|
|
3691
|
-
supersedes: argv[6] ?? null,
|
|
3692
|
-
});
|
|
3590
|
+
} else if (argv[0] === 'note' && argv[1] !== 'list') {
|
|
3591
|
+
const flags = matchFlagCommand(argv, ['note'], {
|
|
3592
|
+
known: ['plan', 'task', 'message', 'input', 'supersedes'],
|
|
3593
|
+
required: ['plan'],
|
|
3594
|
+
});
|
|
3595
|
+
const hasMessage = typeof flags?.message === 'string' && flags.message.length > 0;
|
|
3596
|
+
const hasInput = isAuthoringPathToken(flags?.input);
|
|
3597
|
+
if (flags !== null && isTodoIdentifier(flags.plan)
|
|
3598
|
+
&& (flags.task === undefined || isTodoIdentifier(flags.task))
|
|
3599
|
+
&& (flags.supersedes === undefined || isTodoDigest(flags.supersedes))
|
|
3600
|
+
&& hasMessage !== hasInput) {
|
|
3601
|
+
action = (repoRoot) => appendNote({
|
|
3602
|
+
repoRoot, env, planKey: flags.plan,
|
|
3603
|
+
taskId: flags.task ?? null,
|
|
3604
|
+
message: hasMessage ? flags.message : null,
|
|
3605
|
+
inputRef: hasInput ? flags.input : null,
|
|
3606
|
+
supersedes: flags.supersedes ?? null,
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3693
3609
|
} else if (argv.length === 5 && argv[0] === 'note' && argv[1] === 'list'
|
|
3694
3610
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3]) && argv[4] === '--json') {
|
|
3695
3611
|
action = (repoRoot) => listNotes({ repoRoot, planKey: argv[3], taskId: null });
|
|
@@ -3716,34 +3632,43 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3716
3632
|
repoRoot, env, fromPlanKey: argv[3], fromTaskId: argv[5],
|
|
3717
3633
|
toPlanKey: argv[7], toTaskId: argv[9], reason: argv[11],
|
|
3718
3634
|
});
|
|
3719
|
-
} else if (argv
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
repoRoot, planKey: argv[3], inputRef: argv[5],
|
|
3723
|
-
});
|
|
3724
|
-
} else if (argv.length === 8 && argv[0] === 'structure' && argv[1] === 'input'
|
|
3725
|
-
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
3726
|
-
&& argv[4] === '--input' && isTodoRef(argv[5])
|
|
3727
|
-
&& argv[6] === '--dry-run' && argv[7] === '--json') {
|
|
3728
|
-
action = (repoRoot) => structureInputDryRun({
|
|
3729
|
-
repoRoot, planKey: argv[3], inputRef: argv[5],
|
|
3730
|
-
});
|
|
3731
|
-
} else if (argv.length === 6 && argv[0] === 'structure' && argv[1] === 'input'
|
|
3732
|
-
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
3733
|
-
&& argv[4] === '--input' && isTodoRef(argv[5])) {
|
|
3734
|
-
action = (repoRoot) => structureInput({
|
|
3735
|
-
repoRoot, planKey: argv[3], inputRef: argv[5],
|
|
3635
|
+
} else if (argv[0] === 'independence' && argv[1] === 'compile') {
|
|
3636
|
+
const flags = matchFlagCommand(argv, ['independence', 'compile'], {
|
|
3637
|
+
known: ['plan', 'input'], required: ['plan', 'input'],
|
|
3736
3638
|
});
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3639
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isAuthoringPathToken(flags.input)) {
|
|
3640
|
+
action = (repoRoot) => independenceCompile({
|
|
3641
|
+
repoRoot, planKey: flags.plan, inputRef: flags.input,
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
} else if (argv[0] === 'structure' && argv[1] === 'input') {
|
|
3645
|
+
const flags = matchFlagCommand(argv, ['structure', 'input'], {
|
|
3646
|
+
known: ['plan', 'input', 'dry-run', 'json'],
|
|
3647
|
+
required: ['plan', 'input'],
|
|
3648
|
+
booleans: ['dry-run', 'json'],
|
|
3649
|
+
});
|
|
3650
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isAuthoringPathToken(flags.input)) {
|
|
3651
|
+
action = flags['dry-run'] === true
|
|
3652
|
+
? (repoRoot) => structureInputDryRun({
|
|
3653
|
+
repoRoot, planKey: flags.plan, inputRef: flags.input,
|
|
3654
|
+
})
|
|
3655
|
+
: (repoRoot) => structureInput({
|
|
3656
|
+
repoRoot, planKey: flags.plan, inputRef: flags.input,
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
} else if (argv[0] === 'structure' && argv[1] === 'compile') {
|
|
3660
|
+
const flags = matchFlagCommand(argv, ['structure', 'compile'], {
|
|
3661
|
+
known: ['plan', 'input'], required: ['plan', 'input'],
|
|
3742
3662
|
});
|
|
3663
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isAuthoringPathToken(flags.input)) {
|
|
3664
|
+
action = (repoRoot) => structureCompile({
|
|
3665
|
+
repoRoot, env, planKey: flags.plan, inputRef: flags.input,
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3743
3668
|
} else if (argv.length === 8 && argv[0] === 'structure' && argv[1] === 'realize'
|
|
3744
3669
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
3745
3670
|
&& argv[4] === '--task' && isTodoIdentifier(argv[5])
|
|
3746
|
-
&& argv[6] === '--input' &&
|
|
3671
|
+
&& argv[6] === '--input' && isAuthoringPathToken(argv[7])) {
|
|
3747
3672
|
action = (repoRoot) => structureRealize({
|
|
3748
3673
|
repoRoot, env, planKey: argv[3], taskId: argv[5], inputRef: argv[7],
|
|
3749
3674
|
});
|
|
@@ -3771,11 +3696,11 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3771
3696
|
action = (repoRoot) => seamProposalApply({ repoRoot, planKey: argv[3] });
|
|
3772
3697
|
} else if (argv.length === 7 && argv[0] === 'independence' && argv[1] === 'witness'
|
|
3773
3698
|
&& argv[2] === 'scaffold' && argv[3] === '--plan' && isTodoIdentifier(argv[4])
|
|
3774
|
-
&& argv[5] === '--input' &&
|
|
3699
|
+
&& argv[5] === '--input' && isAuthoringPathToken(argv[6])) {
|
|
3775
3700
|
action = (repoRoot) => witnessScaffold({ repoRoot, planKey: argv[4], inputRef: argv[6] });
|
|
3776
3701
|
} else if (argv.length === 6 && argv[0] === 'seam-proposal' && argv[1] === 'land'
|
|
3777
3702
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
3778
|
-
&& argv[4] === '--names' &&
|
|
3703
|
+
&& argv[4] === '--names' && isAuthoringPathToken(argv[5])) {
|
|
3779
3704
|
action = async (repoRoot) => seamProposalApply({
|
|
3780
3705
|
repoRoot,
|
|
3781
3706
|
planKey: argv[3],
|
|
@@ -3784,7 +3709,7 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3784
3709
|
});
|
|
3785
3710
|
} else if ((argv.length === 5 || argv.length === 6) && argv[0] === 'seam-profile'
|
|
3786
3711
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
3787
|
-
&& argv[3] === '--file' &&
|
|
3712
|
+
&& argv[3] === '--file' && isAuthoringPathToken(argv[4])
|
|
3788
3713
|
&& (argv.length === 5 || argv[5] === '--json')) {
|
|
3789
3714
|
action = (repoRoot) => seamProfile({ repoRoot, planKey: argv[2], filePath: argv[4] });
|
|
3790
3715
|
} else if (argv.length === 4 && argv[0] === 'seam-proposal' && argv[1] === 'compile'
|
|
@@ -3824,32 +3749,46 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3824
3749
|
next_action: 'lattice todo gantt serve --port 0',
|
|
3825
3750
|
});
|
|
3826
3751
|
};
|
|
3827
|
-
} else if (
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
}
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
} else if (argv
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3752
|
+
} else if (argv[0] === 'migrate' && argv[1] !== '--schema') {
|
|
3753
|
+
const flags = matchFlagCommand(argv, ['migrate'], {
|
|
3754
|
+
known: ['input', 'json', 'serialization-reviewed', 'dry-run'],
|
|
3755
|
+
required: ['input'],
|
|
3756
|
+
booleans: ['json', 'serialization-reviewed', 'dry-run'],
|
|
3757
|
+
});
|
|
3758
|
+
if (flags !== null && isAuthoringPathToken(flags.input)) {
|
|
3759
|
+
action = flags['dry-run'] === true
|
|
3760
|
+
? (repoRoot) => migrateDryRun({ repoRoot, inputRef: flags.input })
|
|
3761
|
+
: (repoRoot) => migrate({ repoRoot, inputRef: flags.input });
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
if (action === null && argv[0] === 'revise') {
|
|
3765
|
+
const flags = matchFlagCommand(argv, ['revise'], {
|
|
3766
|
+
known: ['plan', 'input'], required: ['plan', 'input'],
|
|
3767
|
+
});
|
|
3768
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isAuthoringPathToken(flags.input)) {
|
|
3769
|
+
action = (repoRoot) => revise({ repoRoot, env, planKey: flags.plan, inputRef: flags.input });
|
|
3770
|
+
}
|
|
3771
|
+
} else if (argv[0] === 'split') {
|
|
3772
|
+
const flags = matchFlagCommand(argv, ['split'], {
|
|
3773
|
+
known: ['plan', 'input'], required: ['plan', 'input'],
|
|
3774
|
+
});
|
|
3775
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isAuthoringPathToken(flags.input)) {
|
|
3776
|
+
action = (repoRoot) => splitTodo({ repoRoot, env, planKey: flags.plan, inputRef: flags.input });
|
|
3777
|
+
}
|
|
3778
|
+
} else if (argv[0] === 'revise-set') {
|
|
3779
|
+
const flags = matchFlagCommand(argv, ['revise-set'], {
|
|
3780
|
+
known: ['input'], required: ['input'],
|
|
3781
|
+
});
|
|
3782
|
+
if (flags !== null && isAuthoringPathToken(flags.input)) {
|
|
3783
|
+
action = (repoRoot) => reviseSet({ repoRoot, env, inputRef: flags.input });
|
|
3784
|
+
}
|
|
3785
|
+
} else if (argv[0] === 'revise-phase') {
|
|
3786
|
+
const flags = matchFlagCommand(argv, ['revise-phase'], {
|
|
3787
|
+
known: ['plan', 'input'], required: ['plan', 'input'],
|
|
3788
|
+
});
|
|
3789
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isAuthoringPathToken(flags.input)) {
|
|
3790
|
+
action = (repoRoot) => revisePhase({ repoRoot, env, planKey: flags.plan, inputRef: flags.input });
|
|
3791
|
+
}
|
|
3853
3792
|
} else if (argv.length === 4 && argv[0] === 'phase' && argv[1] === 'status'
|
|
3854
3793
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])) {
|
|
3855
3794
|
action = (repoRoot) => phaseStatus({ repoRoot, planKey: argv[3] });
|
|
@@ -3859,13 +3798,17 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3859
3798
|
&& argv[6] === '--reason' && argv[7].length > 0) {
|
|
3860
3799
|
action = (repoRoot) => phaseMutation({ repoRoot, env, planKey: argv[3], phaseId: argv[5],
|
|
3861
3800
|
kind: 'phase_review', payload: { reason: argv[7] } });
|
|
3862
|
-
} else if (argv
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3801
|
+
} else if (argv[0] === 'phase' && ['accept', 'reject'].includes(argv[1])) {
|
|
3802
|
+
const flags = matchFlagCommand(argv, ['phase', argv[1]], {
|
|
3803
|
+
known: ['plan', 'phase', 'input'], required: ['plan', 'phase', 'input'],
|
|
3804
|
+
});
|
|
3805
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.phase)
|
|
3806
|
+
&& isAuthoringPathToken(flags.input)) {
|
|
3807
|
+
action = (repoRoot) => phaseDecision({
|
|
3808
|
+
repoRoot, env, planKey: flags.plan, phaseId: flags.phase,
|
|
3809
|
+
outcome: argv[1], inputRef: flags.input,
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3869
3812
|
} else if ((argv.length === 8 || argv.length === 10) && argv[0] === 'phase'
|
|
3870
3813
|
&& argv[1] === 'reopen' && argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
3871
3814
|
&& argv[4] === '--phase' && isTodoIdentifier(argv[5])
|
|
@@ -3884,56 +3827,98 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3884
3827
|
&& parseBaselineExceptFlags(argv.slice(4)) !== null) {
|
|
3885
3828
|
const exceptPlanKeys = parseBaselineExceptFlags(argv.slice(4));
|
|
3886
3829
|
action = (repoRoot) => phaseBaseline({ repoRoot, env, reason: argv[3], exceptPlanKeys });
|
|
3887
|
-
} else if (
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3830
|
+
} else if (argv[0] === 'start') {
|
|
3831
|
+
const flags = matchFlagCommand(argv, ['start'], {
|
|
3832
|
+
known: ['plan', 'task', 'parallel-frontier', 'override-reason', 'serial-confirmed'],
|
|
3833
|
+
required: ['plan', 'task'],
|
|
3834
|
+
booleans: ['parallel-frontier', 'serial-confirmed'],
|
|
3835
|
+
});
|
|
3836
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
|
|
3837
|
+
&& (flags['override-reason'] === undefined || (typeof flags['override-reason'] === 'string'
|
|
3838
|
+
&& flags['override-reason'].length > 0))) {
|
|
3839
|
+
action = (repoRoot) => startTask({
|
|
3840
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task,
|
|
3841
|
+
overrideReason: typeof flags['override-reason'] === 'string' ? flags['override-reason'] : null,
|
|
3842
|
+
parallelFrontier: flags['parallel-frontier'] === true,
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
} else if (argv[0] === 'done') {
|
|
3846
|
+
const flags = matchFlagCommand(argv, ['done'], {
|
|
3847
|
+
known: ['plan', 'task', 'evidence', 'test-result', 'message'],
|
|
3848
|
+
required: ['plan', 'task'],
|
|
3849
|
+
});
|
|
3850
|
+
const hasEvidence = isAuthoringPathToken(flags?.evidence);
|
|
3851
|
+
const hasMessage = typeof flags?.message === 'string' && flags.message.length > 0;
|
|
3852
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
|
|
3853
|
+
&& hasEvidence !== hasMessage
|
|
3854
|
+
&& (flags['test-result'] === undefined || isAuthoringPathToken(flags['test-result']))) {
|
|
3855
|
+
action = (repoRoot) => mutate({
|
|
3856
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task,
|
|
3857
|
+
kind: 'done', payload: 'authored',
|
|
3858
|
+
evidenceRef: hasEvidence ? flags.evidence : null,
|
|
3859
|
+
evidenceMessage: hasMessage ? flags.message : null,
|
|
3860
|
+
testResultRef: flags['test-result'] ?? null,
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
if (action === null && argv[0] === 'retract') {
|
|
3865
|
+
const flags = matchFlagCommand(argv, ['retract'], {
|
|
3866
|
+
known: ['plan', 'task', 'reason'], required: ['plan', 'task', 'reason'],
|
|
3904
3867
|
});
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3868
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)) {
|
|
3869
|
+
action = (repoRoot) => retractStart({
|
|
3870
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task, reason: flags.reason,
|
|
3871
|
+
});
|
|
3872
|
+
}
|
|
3873
|
+
} else if (argv[0] === 'block') {
|
|
3874
|
+
const flags = matchFlagCommand(argv, ['block'], {
|
|
3875
|
+
known: ['plan', 'task', 'reason'], required: ['plan', 'task', 'reason'],
|
|
3876
|
+
});
|
|
3877
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)) {
|
|
3878
|
+
action = (repoRoot) => mutate({
|
|
3879
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task,
|
|
3880
|
+
kind: 'block', payload: { reason: flags.reason }, evidenceRef: null,
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
} else if (argv[0] === 'unblock') {
|
|
3884
|
+
const flags = matchFlagCommand(argv, ['unblock'], {
|
|
3885
|
+
known: ['plan', 'task'], required: ['plan', 'task'],
|
|
3886
|
+
});
|
|
3887
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)) {
|
|
3888
|
+
action = (repoRoot) => mutate({
|
|
3889
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task,
|
|
3890
|
+
kind: 'unblock', payload: {}, evidenceRef: null,
|
|
3891
|
+
});
|
|
3892
|
+
}
|
|
3893
|
+
} else if (argv[0] === 'evidence' && argv[1] === 'promote') {
|
|
3894
|
+
const flags = matchFlagCommand(argv, ['evidence', 'promote'], {
|
|
3895
|
+
known: ['plan', 'task', 'evidence'], required: ['plan', 'task', 'evidence'],
|
|
3896
|
+
});
|
|
3897
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
|
|
3898
|
+
&& isAuthoringPathToken(flags.evidence)) {
|
|
3899
|
+
action = (repoRoot) => mutate({
|
|
3900
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task,
|
|
3901
|
+
kind: 'done', payload: 'evidence_promotion', evidenceRef: flags.evidence,
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
} else if (argv[0] === 'reopen') {
|
|
3905
|
+
const flags = matchFlagCommand(argv, ['reopen'], {
|
|
3906
|
+
known: ['plan', 'task', 'reason', 'override-reason'],
|
|
3907
|
+
required: ['plan', 'task', 'reason'],
|
|
3908
|
+
});
|
|
3909
|
+
if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)
|
|
3910
|
+
&& (flags['override-reason'] === undefined
|
|
3911
|
+
|| (typeof flags['override-reason'] === 'string' && flags['override-reason'].length > 0))) {
|
|
3912
|
+
action = (repoRoot) => mutate({
|
|
3913
|
+
repoRoot, env, planKey: flags.plan, taskId: flags.task,
|
|
3914
|
+
kind: 'reopen',
|
|
3915
|
+
payload: {
|
|
3916
|
+
reason: flags.reason,
|
|
3917
|
+
override_reason: typeof flags['override-reason'] === 'string' ? flags['override-reason'] : null,
|
|
3918
|
+
},
|
|
3919
|
+
evidenceRef: null,
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3937
3922
|
}
|
|
3938
3923
|
if (action === null) {
|
|
3939
3924
|
const command = typeof argv[0] === 'string' ? argv[0] : null;
|