@quolu/lattice 0.59.1 → 0.60.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/src/todo-cli.mjs CHANGED
@@ -88,6 +88,7 @@ import { compileTodoStructureOverlay } from './todo-structure-overlay.mjs';
88
88
  import {
89
89
  buildTodoStructureCompileArtifact,
90
90
  projectTodoStructureEffective,
91
+ readTodoStructureArtifactDiagnostics,
91
92
  readTodoStructureFinalizationState,
92
93
  readTodoStructureFinalizationsForStatus,
93
94
  readTodoStructureState,
@@ -97,7 +98,9 @@ import {
97
98
  appendTodoExtraction,
98
99
  compileTodoExtraction,
99
100
  explainTodoExtraction,
101
+ isTodoExtractionConnectionOnly,
100
102
  TODO_EXTRACTION_SCHEMA_V3,
103
+ TODO_EXTRACTION_SCHEMA_V4,
101
104
  validateTodoExtraction,
102
105
  } from './todo-migration.mjs';
103
106
  import {
@@ -139,6 +142,7 @@ import {
139
142
  } from './seam-proposal-contracts.mjs';
140
143
  import { compileSeamProposalArtifact, declaredConcernSymbols } from './seam-proposal.mjs';
141
144
  import { collectSensorEvidence } from './sensor-adapter.mjs';
145
+ import { collectTodoIndependenceAuthoritativeObservation } from './todo-independence-authoritative-observation.mjs';
142
146
  import { applySeamProposal } from './seam-apply.mjs';
143
147
  import { todoPlanPrecedences } from './seam-verification.mjs';
144
148
  import {
@@ -188,7 +192,7 @@ const TODO_SCHEMA_COMMANDS = Object.freeze({
188
192
  'revise-phase': {
189
193
  title: 'lattice.phase_todo_revision.v3', file: 'lattice.phase_todo_revision.v3.schema.json',
190
194
  },
191
- migrate: { title: 'lattice.todo_extraction.v3', file: 'lattice.todo_extraction.v3.schema.json' },
195
+ migrate: { title: 'lattice.todo_extraction.v4', file: 'lattice.todo_extraction.v4.schema.json' },
192
196
  structure: {
193
197
  title: 'lattice.todo_structure_set.v1', file: 'lattice.todo_structure_set.v1.schema.json',
194
198
  },
@@ -418,9 +422,9 @@ async function readMigrationInput(repoRoot, inputRef, { requireValid = true } =
418
422
  throw new TodoStoreError('INVALID_JSON', 'json_parse_failed');
419
423
  }
420
424
  if (!requireValid) return extraction;
421
- if (extraction?.schema !== TODO_EXTRACTION_SCHEMA_V3) {
422
- throw new TodoStoreError('DESIGN_MEMO_REQUIRED', 'todo_extraction_v3_required', undefined, {
423
- design_memo_prompt: TODO_DESIGN_MEMO_PROMPT,
425
+ if (![TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(extraction?.schema)) {
426
+ throw new TodoStoreError('INVALID_TODO_EXTRACTION', 'todo_extraction_schema_unsupported', undefined, {
427
+ expected: TODO_EXTRACTION_SCHEMA_V4,
424
428
  next_action: 'lattice todo migrate --schema --json',
425
429
  });
426
430
  }
@@ -1256,21 +1260,42 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
1256
1260
  });
1257
1261
  }
1258
1262
  const registered = extraction.tasks.filter(({ disposition }) => disposition.startsWith('register_'));
1259
- if (registered.length === 0) throw new TodoStoreError('MIGRATION_EMPTY', 'no_registered_tasks');
1263
+ const connectionOnly = isTodoExtractionConnectionOnly(extraction);
1264
+ if (registered.length === 0 && !connectionOnly) {
1265
+ throw new TodoStoreError('MIGRATION_EMPTY', 'no_registered_tasks');
1266
+ }
1260
1267
 
1261
- const dispatchShape = computeTodoDispatchShapeForPlan({
1268
+ const dispatchShape = connectionOnly ? null : computeTodoDispatchShapeForPlan({
1262
1269
  projectId: extraction.project_id,
1263
1270
  planKey: extraction.plan_key,
1264
1271
  taskIds: registered.map(({ task_id: taskId }) => taskId),
1265
1272
  hardDependencies: extraction.hard_dependencies,
1266
1273
  joins: extraction.joins,
1267
1274
  });
1268
- assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
1275
+ if (dispatchShape !== null) {
1276
+ assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
1277
+ }
1269
1278
 
1270
1279
  const imported = await appendTodoExtraction({ repoRoot, extraction });
1280
+ let companion = null;
1281
+ if ((imported.crossPlanDependencies ?? []).length > 0) {
1282
+ const connectedStore = await readTodoStore({ repoRoot });
1283
+ const event = imported.crossPlanDependencies[0];
1284
+ companion = {
1285
+ repair: event.payload.from,
1286
+ target: event.payload.to,
1287
+ reason: event.payload.reason,
1288
+ event_digest: event.event_digest,
1289
+ connected_frontier: computeReadyFrontier(connectedStore),
1290
+ next_action: imported.connectionOnly === true ? 'lattice todo status --json'
1291
+ : isPhaselessTodoPlanSchema(imported.plan.schema)
1292
+ ? `lattice todo revise-phase --plan ${imported.plan.plan_key} --input <phase-revision.json>`
1293
+ : 'lattice todo status --json',
1294
+ };
1295
+ }
1271
1296
  const result = {
1272
- // ob03: 調整方式の案内をv3で足す。ADR 0054のとおり既存versionへのin-place追加はしない。
1273
- schema: 'lattice.todo_migrate_result.v3',
1297
+ // companionを足す時点でv4へ上げ、通常移行もnullを常在させてresult shapeを固定する。
1298
+ schema: 'lattice.todo_migrate_result.v4',
1274
1299
  project_id: imported.plan.project_id,
1275
1300
  plan_key: imported.plan.plan_key,
1276
1301
  plan_version: imported.plan.plan_version,
@@ -1282,7 +1307,7 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
1282
1307
  snapshot_ref: imported.descriptor.snapshot_ref,
1283
1308
  topology_digest: imported.plan.topology_digest,
1284
1309
  journal_head_digest: imported.events.at(-1).event_digest,
1285
- dispatch_shape: {
1310
+ dispatch_shape: dispatchShape === null ? null : {
1286
1311
  task_count: dispatchShape.task_count,
1287
1312
  critical_path_length: dispatchShape.critical_path_length,
1288
1313
  max_frontier_width: dispatchShape.max_frontier_width,
@@ -1291,8 +1316,8 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
1291
1316
  // ADR 0147裁定3: phase無しplanの作成は拒否せず、終端監査が要ることを結果へ明示するに
1292
1317
  // 留める。extraction経由のmigrateは常にphase無しplan(todo_plan.v2)を作るが、将来の
1293
1318
  // 拡張に備えisPhaselessTodoPlanSchemaで動的に判定する。
1294
- terminal_audit_required: isPhaselessTodoPlanSchema(imported.plan.schema),
1295
- phase_guidance: isPhaselessTodoPlanSchema(imported.plan.schema) ? {
1319
+ terminal_audit_required: imported.connectionOnly !== true && isPhaselessTodoPlanSchema(imported.plan.schema),
1320
+ phase_guidance: imported.connectionOnly !== true && isPhaselessTodoPlanSchema(imported.plan.schema) ? {
1296
1321
  capability: 'acquire_phase',
1297
1322
  preserves_completed_state: true,
1298
1323
  schema_command: 'lattice todo revise-phase --schema --json',
@@ -1301,11 +1326,12 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
1301
1326
  } : null,
1302
1327
  // ob03: 起票直後のplanは必ず調整方式が未宣言である。ここで案内しないと、選ぶ機会が
1303
1328
  // 「誰も呼ぶ動機の無いdrilldown」にしか無くなる——前campaignの監査待ちと同じ形になる。
1304
- coordination_guidance: {
1329
+ coordination_guidance: imported.connectionOnly === true ? null : {
1305
1330
  mode: null,
1306
1331
  modes: [...TODO_COORDINATION_MODES],
1307
1332
  next_action: `lattice todo independence mode --plan ${imported.plan.plan_key} --set <witness|conversation> --reason <text>`,
1308
1333
  },
1334
+ companion,
1309
1335
  result_digest: '',
1310
1336
  };
1311
1337
  result.result_digest = todoSelfDigest(result, 'result_digest');
@@ -1399,8 +1425,9 @@ function extractionAuthoringViolations(extraction, now = new Date()) {
1399
1425
  }
1400
1426
  const registered = new Set(tasks.filter(({ disposition }) => typeof disposition === 'string'
1401
1427
  && disposition.startsWith('register_')).map(({ task_id: taskId }) => taskId));
1428
+ const connectionOnly = isTodoExtractionConnectionOnly(extraction);
1402
1429
  const unresolvedLocal = (ref) => ref?.project_id === extraction?.project_id
1403
- && ref?.plan_key === extraction?.plan_key && !registered.has(ref?.task_id);
1430
+ && ref?.plan_key === extraction?.plan_key && !connectionOnly && !registered.has(ref?.task_id);
1404
1431
  for (const [index, edge] of edges.entries()) {
1405
1432
  for (const side of ['from', 'to']) {
1406
1433
  if (unresolvedLocal(edge?.[side])) {
@@ -1425,12 +1452,15 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
1425
1452
  const extraction = await readMigrationInput(repoRoot, inputRef, { requireValid: false });
1426
1453
  const violations = [];
1427
1454
  const tasks = Array.isArray(extraction?.tasks) ? extraction.tasks : [];
1428
- const invalidMemos = tasks.map((task, index) => ({
1455
+ const designMemoSchema = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4].includes(extraction?.schema);
1456
+ const invalidMemos = (designMemoSchema ? tasks : []).map((task, index) => ({
1429
1457
  task, index, explained: explainTodoDesignMemo(task?.design_memo),
1430
1458
  })).filter(({ explained }) => !explained.valid).slice(0, 64);
1431
- if (extraction?.schema !== TODO_EXTRACTION_SCHEMA_V3) {
1459
+ const supportedSchema = [TODO_EXTRACTION_SCHEMA_V3, TODO_EXTRACTION_SCHEMA_V4]
1460
+ .includes(extraction?.schema);
1461
+ if (!supportedSchema) {
1432
1462
  violations.push({ code: 'schema_retired', path: '/schema', task_ids: [],
1433
- expected: TODO_EXTRACTION_SCHEMA_V3,
1463
+ expected: TODO_EXTRACTION_SCHEMA_V4,
1434
1464
  actual: typeof extraction?.schema === 'string' ? extraction.schema
1435
1465
  : { type: typeof extraction?.schema },
1436
1466
  next_action: 'lattice todo migrate --schema --json' });
@@ -1441,8 +1471,7 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
1441
1471
  expected: explained.expected, actual: explained.actual,
1442
1472
  prompt: TODO_DESIGN_MEMO_PROMPT, next_action: 'lattice todo migrate --schema --json' });
1443
1473
  }
1444
- const schemaValid = extraction?.schema === TODO_EXTRACTION_SCHEMA_V3
1445
- && validateTodoExtraction(extraction);
1474
+ const schemaValid = supportedSchema && validateTodoExtraction(extraction);
1446
1475
  if (!schemaValid) {
1447
1476
  const explained = explainTodoExtraction(extraction);
1448
1477
  if (!explained.valid && !explained.path.endsWith('/design_memo')) {
@@ -1467,28 +1496,38 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
1467
1496
  }
1468
1497
  const registered = tasks.filter(({ disposition }) => typeof disposition === 'string'
1469
1498
  && disposition.startsWith('register_'));
1470
- if (registered.length === 0) {
1499
+ const connectionOnly = schemaValid && isTodoExtractionConnectionOnly(extraction);
1500
+ if (registered.length === 0 && !connectionOnly) {
1471
1501
  violations.push({ code: 'no_registered_tasks', path: '/tasks', task_ids: [],
1472
1502
  next_action: 'register_at_least_one_task' });
1473
1503
  }
1474
1504
 
1475
1505
  let dispatchShape = null;
1476
1506
  let plannedPlan = null;
1477
- if (schemaValid && unresolvedTaskIds.length === 0 && registered.length > 0) {
1507
+ let plannedConnection = null;
1508
+ if (schemaValid && unresolvedTaskIds.length === 0 && (registered.length > 0 || connectionOnly)) {
1478
1509
  try {
1479
1510
  const compiled = compileTodoExtraction(extraction, repoRoot);
1480
- plannedPlan = buildTodoPlan(compiled.plan);
1481
- dispatchShape = computeTodoDispatchShapeForPlan({
1482
- projectId: extraction.project_id, planKey: extraction.plan_key,
1483
- taskIds: registered.map(({ task_id: taskId }) => taskId),
1484
- hardDependencies: extraction.hard_dependencies, joins: extraction.joins,
1485
- });
1486
- try {
1487
- assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
1488
- } catch (error) {
1489
- violations.push({ code: error?.detail?.reason ?? 'plan_shape_too_serial', path: '/hard_dependencies',
1490
- task_ids: error?.detail?.critical_path_task_ids ?? [],
1491
- next_action: 'reconsider_parallel_seams_or_pass_serialization_reviewed' });
1511
+ if (connectionOnly) {
1512
+ plannedConnection = {
1513
+ source: compiled.crossPlanDependencies[0].from,
1514
+ target: compiled.crossPlanDependencies[0].to,
1515
+ reason: compiled.crossPlanDependencies[0].reason,
1516
+ };
1517
+ } else {
1518
+ plannedPlan = buildTodoPlan(compiled.plan);
1519
+ dispatchShape = computeTodoDispatchShapeForPlan({
1520
+ projectId: extraction.project_id, planKey: extraction.plan_key,
1521
+ taskIds: registered.map(({ task_id: taskId }) => taskId),
1522
+ hardDependencies: extraction.hard_dependencies, joins: extraction.joins,
1523
+ });
1524
+ try {
1525
+ assertTodoDispatchShapeReviewed({ shape: dispatchShape, reviewed: serializationReviewed });
1526
+ } catch (error) {
1527
+ violations.push({ code: error?.detail?.reason ?? 'plan_shape_too_serial', path: '/hard_dependencies',
1528
+ task_ids: error?.detail?.critical_path_task_ids ?? [],
1529
+ next_action: 'reconsider_parallel_seams_or_pass_serialization_reviewed' });
1530
+ }
1492
1531
  }
1493
1532
  } catch (error) {
1494
1533
  plannedPlan = null;
@@ -1523,13 +1562,19 @@ async function migrateDryRun({ repoRoot, inputRef, serializationReviewed = false
1523
1562
  }
1524
1563
  const bounded = violations.slice(0, 64);
1525
1564
  const result = {
1526
- schema: 'lattice.todo_migrate_dry_run_result.v1',
1565
+ schema: 'lattice.todo_migrate_dry_run_result.v2',
1527
1566
  valid: bounded.length === 0,
1528
1567
  project_id: isTodoIdentifier(extraction?.project_id) ? extraction.project_id : null,
1529
1568
  plan_key: isTodoIdentifier(extraction?.plan_key) ? extraction.plan_key : null,
1530
1569
  violations: bounded,
1531
1570
  overflow_count: Math.max(0, violations.length - bounded.length),
1532
- planned: plannedPlan === null ? null : {
1571
+ planned: plannedConnection !== null ? {
1572
+ connection_only: true,
1573
+ source: plannedConnection.source,
1574
+ target: plannedConnection.target,
1575
+ reason: plannedConnection.reason,
1576
+ } : plannedPlan === null ? null : {
1577
+ connection_only: false,
1533
1578
  plan_schema: plannedPlan.schema,
1534
1579
  task_count: registered.length,
1535
1580
  topology_digest: plannedPlan.topology_digest,
@@ -1649,13 +1694,15 @@ async function splitTodo({ repoRoot, env, planKey, inputRef }) {
1649
1694
 
1650
1695
  async function status({ repoRoot }) {
1651
1696
  const store = await readTodoStore({ repoRoot });
1652
- return projectTodoStatus(store, {
1697
+ const result = projectTodoStatus(store, {
1653
1698
  planNotes: await readTodoPlanNotesForStatus({ repoRoot, store }),
1654
1699
  parallelCandidates: await readTodoParallelCandidatesForStatus({
1655
1700
  repoRoot, store, gitHead: currentHeadSha, changedPathsSince,
1656
1701
  }),
1657
1702
  structureFinalizations: await readTodoStructureFinalizationsForStatus({ repoRoot, store }),
1658
1703
  });
1704
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1705
+ return result;
1659
1706
  }
1660
1707
 
1661
1708
  async function adoptDashboardRoot({ repoRoot, env }) {
@@ -2468,8 +2515,8 @@ async function structure({ repoRoot, requestedPlanKey }) {
2468
2515
 
2469
2516
  async function independenceCompile({ repoRoot, planKey, inputRef }) {
2470
2517
  const witnessSet = await readWitnessSetInput(repoRoot, inputRef);
2471
- requireCleanWorktree(repoRoot);
2472
- const baseSha = currentHeadSha(repoRoot);
2518
+ const observation = await collectTodoIndependenceAuthoritativeObservation({ repoRoot, witnessSet });
2519
+ const baseSha = observation.head_sha;
2473
2520
  const store = await readTodoStore({ repoRoot });
2474
2521
  const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
2475
2522
  if (!member) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, { plan_key: planKey });
@@ -2484,7 +2531,7 @@ async function independenceCompile({ repoRoot, planKey, inputRef }) {
2484
2531
  plan: member.plan,
2485
2532
  baseSha,
2486
2533
  compiledAt: new Date().toISOString(),
2487
- sensorEvidence: await collectWitnessSensorEvidence({ cwd: repoRoot, witnessSet }),
2534
+ sensorEvidence: observation.sensor_evidence,
2488
2535
  previousArtifact,
2489
2536
  });
2490
2537
  const { ref } = await writeTodoIndependenceArtifact({ repoRoot, artifact });
@@ -3555,6 +3602,20 @@ async function verify({ repoRoot, requestedPlanKey }) {
3555
3602
  },
3556
3603
  };
3557
3604
  });
3605
+ const structureDiagnostics = [];
3606
+ for (const member of members) {
3607
+ const diagnostics = await readTodoStructureArtifactDiagnostics({
3608
+ repoRoot,
3609
+ store: { ...store, members: [member] },
3610
+ });
3611
+ structureDiagnostics.push(...diagnostics);
3612
+ }
3613
+ if (structureDiagnostics.length > 0) {
3614
+ throw new TodoStoreError(
3615
+ 'STRUCTURE_ARTIFACT_INVALID', structureDiagnostics[0].reason, undefined,
3616
+ { diagnostics: structureDiagnostics },
3617
+ );
3618
+ }
3558
3619
  const result = {
3559
3620
  schema: 'lattice.todo_verify_result.v3',
3560
3621
  project_id: store.project_id,
@@ -3857,10 +3918,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3857
3918
  action = (repoRoot) => migrateDryRun({
3858
3919
  repoRoot, inputRef: argv[2], serializationReviewed: argv.length === 6,
3859
3920
  });
3860
- } else if ((argv.length === 3 || argv.length === 4) && argv[0] === 'migrate' && argv[1] === '--input'
3861
- && isTodoRef(argv[2]) && (argv.length === 3 || argv[3] === '--serialization-reviewed')) {
3921
+ } else if ((argv.length === 3 || argv.length === 4 || argv.length === 5)
3922
+ && argv[0] === 'migrate' && argv[1] === '--input' && isTodoRef(argv[2])
3923
+ && (argv.length === 3
3924
+ || (argv.length === 4 && ['--json', '--serialization-reviewed'].includes(argv[3]))
3925
+ || (argv.length === 5 && argv[3] === '--serialization-reviewed' && argv[4] === '--json'))) {
3862
3926
  action = (repoRoot) => migrate({
3863
- repoRoot, inputRef: argv[2], serializationReviewed: argv.length === 4,
3927
+ repoRoot, inputRef: argv[2], serializationReviewed: argv[3] === '--serialization-reviewed',
3864
3928
  });
3865
3929
  } else if (argv.length === 5 && argv[0] === 'revise'
3866
3930
  && argv[1] === '--plan' && isTodoIdentifier(argv[2])
@@ -0,0 +1,116 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { gitSpawnSync } from './git-process.mjs';
6
+ import { runSensorCli } from './sensor-cli.mjs';
7
+ import { collectWitnessSensorEvidence } from './todo-independence.mjs';
8
+
9
+ export class TodoIndependenceObservationError extends Error {
10
+ constructor(code, reason, detail = {}) {
11
+ super(reason);
12
+ this.name = 'TodoIndependenceObservationError';
13
+ this.code = code;
14
+ this.detail = { reason, ...detail };
15
+ }
16
+ }
17
+
18
+ function fail(code, reason, detail = {}) {
19
+ throw new TodoIndependenceObservationError(code, reason, detail);
20
+ }
21
+
22
+ function git({ cwd, args, operation }) {
23
+ const result = gitSpawnSync(args, {
24
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
25
+ });
26
+ if (result.error !== undefined || result.status !== 0 || result.signal !== null) {
27
+ fail('INDEPENDENCE_OBSERVATION_GIT_FAILED', 'observation_git_command_failed', {
28
+ operation, status: result.status ?? null, signal: result.signal ?? null,
29
+ cause: result.error?.message ?? null,
30
+ });
31
+ }
32
+ return result.stdout.trim();
33
+ }
34
+
35
+ function memoryStream() {
36
+ let value = '';
37
+ return {
38
+ stream: { write: (chunk) => { value += String(chunk); } },
39
+ read: () => value,
40
+ };
41
+ }
42
+
43
+ async function initializeSensor(observationRoot) {
44
+ const stdout = memoryStream();
45
+ const stderr = memoryStream();
46
+ const status = await runSensorCli({
47
+ argv: ['init', observationRoot, '--json'],
48
+ stdout: stdout.stream,
49
+ stderr: stderr.stream,
50
+ });
51
+ if (status !== 0) {
52
+ fail('INDEPENDENCE_OBSERVATION_SENSOR_INIT_FAILED', 'observation_sensor_init_failed', {
53
+ sensor_error: stderr.read(),
54
+ });
55
+ }
56
+ }
57
+
58
+ /** 共有repoのdirtyな状態を観測せず、current HEADのclean worktreeでsensorを実行する。 */
59
+ export async function collectTodoIndependenceAuthoritativeObservation({ repoRoot, witnessSet } = {}) {
60
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0
61
+ || witnessSet === null || typeof witnessSet !== 'object') {
62
+ fail('INDEPENDENCE_OBSERVATION_INPUT_INVALID', 'observation_input_invalid');
63
+ }
64
+
65
+ const headSha = git({
66
+ cwd: repoRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'], operation: 'resolve_head',
67
+ });
68
+ const temporaryRoot = await mkdtemp(path.join(tmpdir(), 'lattice-independence-observation-'));
69
+ const observationRoot = path.join(temporaryRoot, 'worktree');
70
+ let registered = false;
71
+ let operationError = null;
72
+ let result;
73
+ try {
74
+ git({
75
+ cwd: repoRoot,
76
+ args: ['worktree', 'add', '--detach', '--quiet', observationRoot, headSha],
77
+ operation: 'worktree_add',
78
+ });
79
+ registered = true;
80
+ const observedHead = git({
81
+ cwd: observationRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'],
82
+ operation: 'verify_observation_head',
83
+ });
84
+ if (observedHead !== headSha) {
85
+ fail('INDEPENDENCE_OBSERVATION_HEAD_MISMATCH', 'observation_head_mismatch', {
86
+ expected_head_sha: headSha, actual_head_sha: observedHead,
87
+ });
88
+ }
89
+ await initializeSensor(observationRoot);
90
+ result = await collectWitnessSensorEvidence({ cwd: observationRoot, witnessSet });
91
+ } catch (error) {
92
+ operationError = error;
93
+ }
94
+
95
+ let cleanupError = null;
96
+ try {
97
+ if (registered) {
98
+ git({
99
+ cwd: repoRoot,
100
+ args: ['worktree', 'remove', '--force', observationRoot],
101
+ operation: 'worktree_remove',
102
+ });
103
+ }
104
+ await rm(temporaryRoot, { recursive: true, force: true });
105
+ } catch (error) {
106
+ cleanupError = error;
107
+ }
108
+ if (cleanupError !== null) {
109
+ fail('INDEPENDENCE_OBSERVATION_CLEANUP_FAILED', 'observation_cleanup_failed', {
110
+ operation_error: operationError?.message ?? null,
111
+ cleanup_error: cleanupError?.message ?? String(cleanupError),
112
+ });
113
+ }
114
+ if (operationError !== null) throw operationError;
115
+ return { head_sha: headSha, sensor_evidence: result };
116
+ }
@@ -154,6 +154,19 @@ const CATALOG = Object.freeze({
154
154
  }),
155
155
  });
156
156
 
157
+ const PULL_INTAKE_READINESS = Object.freeze({
158
+ required: Object.freeze({
159
+ code: 'pull_independence_required',
160
+ message: 'pull設備のintakeには有効なindependence artifactが必須である。conversation調整はwitness督促を消すが、この実行前提は満たさない。',
161
+ next_action: 'compile_independence_or_choose_non_pull_execution',
162
+ }),
163
+ ready: Object.freeze({
164
+ code: 'pull_independence_ready',
165
+ message: 'pull設備が読むplan-level independence bindingは有効である。task固有境界とruntime競合はintake時に検査する。',
166
+ next_action: 'none',
167
+ }),
168
+ });
169
+
157
170
  /** 切断可能性の言い換え。conflictの案内へ添える。 */
158
171
  const SEVERABILITY_HINT = Object.freeze({
159
172
  code_seam: 'symbol/pathの衝突なので、境界を分けるrefactorで並列化しうる。記録済みの競合からseam-proposal compileを検討できる。',
@@ -173,6 +186,18 @@ export function todoIndependenceGuidance(code, { severability = null } = {}) {
173
186
  };
174
187
  }
175
188
 
189
+ export function pullIntakeReadinessGuidance({ coordinationMode = null, ready }) {
190
+ if (![null, 'witness', 'conversation'].includes(coordinationMode)
191
+ || typeof ready !== 'boolean') {
192
+ throw new TypeError('pull intake readiness input is invalid');
193
+ }
194
+ return {
195
+ ...(ready ? PULL_INTAKE_READINESS.ready : PULL_INTAKE_READINESS.required),
196
+ plan_binding_ready: ready,
197
+ coordination_mode: coordinationMode,
198
+ };
199
+ }
200
+
176
201
  /**
177
202
  * 記録済みの宣言膨張を、AIが分割を検討するための助言へ写す。
178
203
  *