@quolu/lattice 0.66.1 → 0.67.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.66.1",
3
+ "version": "0.67.1",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
package/src/cli-help.mjs CHANGED
@@ -88,6 +88,7 @@ Read commands:
88
88
  Write commands:
89
89
  repair-eol --json # Git checkoutでCRLF化された既存storeをcanonical LFへ戻し、EOL保護を追加する
90
90
  dashboard adopt --json # 衝突したproject_idの配信元rootを現在repoへ明示的に移す
91
+ dashboard ensure --json # daemon生存保証の冪等入口(launchd等の周期実行向け)
91
92
  dashboard remove <project_id> --json # 登録簿から1件外す(対象repoの外からも叩ける)
92
93
  note --plan <key> [--task <id>] (--message <text>|--input <file>)
93
94
  # ToDoへ作業継続に必要な方針・調査結果・注意をappend-onlyで追記する
@@ -110,6 +111,7 @@ Write commands:
110
111
  evidence promote --plan <key> --task <id> --evidence <file>
111
112
  # done状態と完了時刻を維持し、最新doneへ追記eventで証拠を再束縛する
112
113
  dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
114
+ dependency disconnect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
113
115
  # 開発中に発見した依存を明示接続する(同一plan内・plan跨ぎとも)。依存の自動推定は行わない
114
116
  independence compile --plan <key> --input <file> # witness setとsensorから並列可否を記録する
115
117
  independence witness migrate --plan <key> # revision後の宣言をtask migrationで写す
@@ -257,7 +259,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
257
259
  'todo snapshot': 'todo snapshot --rebuild --plan <key>',
258
260
  'todo gantt': 'todo gantt serve --port <port> [--scope live|all] # 動的表示のみ。静的HTML生成は廃止',
259
261
  'todo gantt serve': 'todo gantt serve --port <0..65535> [--scope live|all] # loopback動的viewer',
260
- 'todo dashboard': 'todo dashboard adopt --json | remove <project_id> --json'
262
+ 'todo dashboard': 'todo dashboard adopt --json | ensure --json | remove <project_id> --json'
261
263
  + ' # 配信元rootの衝突を明示的に解消/登録簿から1件外す',
262
264
  'todo dashboard adopt': 'todo dashboard adopt --json # 現在repoをproject_idの配信元として明示採用',
263
265
  'todo dashboard remove': 'todo dashboard remove <project_id> --json # 登録簿から1件外す(対象repo不在でも可)',
@@ -281,6 +283,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
281
283
  'todo evidence promote': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
282
284
  'todo dependency': 'todo dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>',
283
285
  'todo dependency connect': 'todo dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>',
286
+ 'todo dependency disconnect': 'todo dependency disconnect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>',
284
287
  'todo split': 'todo split --plan <key> --input <file>',
285
288
  'todo revise': 'todo revise --plan <key> --input <file> | --schema --json',
286
289
  'todo revise-phase': 'todo revise-phase --plan <key> --input <file> | --schema --json',
package/src/todo-cli.mjs CHANGED
@@ -1047,6 +1047,44 @@ async function dependencyConnect({
1047
1047
  return result;
1048
1048
  }
1049
1049
 
1050
+ // connectの対。誤って張った線の除去(tombstone)。端点で一意に生きた接続を特定し、
1051
+ // その接続eventが積まれているplanのplan-scoped chainへ除去eventを積む。
1052
+ async function dependencyDisconnect({
1053
+ repoRoot, env, fromPlanKey, fromTaskId, toPlanKey, toTaskId, reason,
1054
+ }) {
1055
+ const store = await readTodoStore({ repoRoot });
1056
+ const live = projectTodoCrossPlanDependencies(store.members).filter((dependency) => (
1057
+ dependency.from.plan_key === fromPlanKey && dependency.from.task_id === fromTaskId
1058
+ && dependency.to.plan_key === toPlanKey && dependency.to.task_id === toTaskId
1059
+ ));
1060
+ if (live.length === 0) {
1061
+ throw new TodoStoreError('DEPENDENCY_INVALID', 'cross_plan_dependency_not_found', undefined, {
1062
+ from: { plan_key: fromPlanKey, task_id: fromTaskId },
1063
+ to: { plan_key: toPlanKey, task_id: toTaskId },
1064
+ });
1065
+ }
1066
+ const [target] = live;
1067
+ const owner = store.members.find((member) => (member.plan_scoped?.events ?? [])
1068
+ .some((event) => event.event_digest === target.event_digest));
1069
+ const { event } = await appendTodoEvent({
1070
+ repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey: owner.plan.plan_key,
1071
+ event: {
1072
+ kind: 'cross_plan_dependency_removed', actor: mutationActor(env),
1073
+ payload: { target_event_digest: target.event_digest, reason },
1074
+ },
1075
+ });
1076
+ const result = {
1077
+ schema: 'lattice.todo_dependency_disconnect_result.v1', project_id: store.project_id,
1078
+ from: target.from, to: target.to, reason,
1079
+ removed_event_digest: target.event_digest,
1080
+ disconnected_by: event.actor, disconnected_at: event.recorded_at,
1081
+ event_digest: event.event_digest,
1082
+ result_digest: '',
1083
+ };
1084
+ result.result_digest = todoSelfDigest(result, 'result_digest');
1085
+ return result;
1086
+ }
1087
+
1050
1088
  async function phaseStatus({ repoRoot, planKey }) {
1051
1089
  const store = await readTodoStore({ repoRoot });
1052
1090
  const [member] = selectMembers(store, planKey);
@@ -3434,7 +3472,7 @@ function writesTodoStore(argv) {
3434
3472
  || (second === 'input' && !argv.includes('--dry-run'));
3435
3473
  case 'seam-proposal': return ['compile', 'apply', 'land'].includes(second);
3436
3474
  case 'evidence': return second === 'promote';
3437
- case 'dependency': return second === 'connect';
3475
+ case 'dependency': return ['connect', 'disconnect'].includes(second);
3438
3476
  case 'phase': return second !== 'status';
3439
3477
  case 'start': case 'retract': case 'block': case 'unblock': case 'retire': case 'done':
3440
3478
  case 'reopen': case 'split': case 'revise': case 'revise-phase': case 'revise-set':
@@ -3657,6 +3695,16 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3657
3695
  } else if (argv.length === 3 && argv[0] === 'dashboard'
3658
3696
  && argv[1] === 'adopt' && argv[2] === '--json') {
3659
3697
  action = (repoRoot) => adoptDashboardRoot({ repoRoot, env });
3698
+ } else if (argv.length === 3 && argv[0] === 'dashboard'
3699
+ && argv[1] === 'ensure' && argv[2] === '--json') {
3700
+ // 生存保証の独立入口(2026-08-29 オーナー裁定「死んだら自動で起こせ」)。従来は書込み
3701
+ // コマンドの副作用でしか daemon を ensure できず、OS再起動後に誰も書込まなければ
3702
+ // 公開工程図が503のまま沈黙した。launchd等の周期実行から叩く冪等な入口。
3703
+ action = async (repoRoot) => {
3704
+ const ensured = await ensureActiveProjectDashboard({ repoRoot, env });
3705
+ return { schema: 'lattice.todo_dashboard_ensure_result.v1',
3706
+ ensured: ensured !== null, activity: ensured };
3707
+ };
3660
3708
  } else if (argv.length === 6 && argv[0] === 'show'
3661
3709
  && argv[1] === '--plan' && isTodoIdentifier(argv[2])
3662
3710
  && argv[3] === '--task' && isTodoIdentifier(argv[4]) && argv[5] === '--json') {
@@ -3713,6 +3761,16 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3713
3761
  repoRoot, env, fromPlanKey: argv[3], fromTaskId: argv[5],
3714
3762
  toPlanKey: argv[7], toTaskId: argv[9], reason: argv[11],
3715
3763
  });
3764
+ } else if (argv.length === 12 && argv[0] === 'dependency' && argv[1] === 'disconnect'
3765
+ && argv[2] === '--from-plan' && isTodoIdentifier(argv[3])
3766
+ && argv[4] === '--from-task' && isTodoIdentifier(argv[5])
3767
+ && argv[6] === '--to-plan' && isTodoIdentifier(argv[7])
3768
+ && argv[8] === '--to-task' && isTodoIdentifier(argv[9])
3769
+ && argv[10] === '--reason' && argv[11].length > 0) {
3770
+ action = (repoRoot) => dependencyDisconnect({
3771
+ repoRoot, env, fromPlanKey: argv[3], fromTaskId: argv[5],
3772
+ toPlanKey: argv[7], toTaskId: argv[9], reason: argv[11],
3773
+ });
3716
3774
  } else if (argv[0] === 'independence' && argv[1] === 'compile') {
3717
3775
  const flags = matchFlagCommand(argv, ['independence', 'compile'], {
3718
3776
  known: ['plan', 'input'], required: ['plan', 'input'],
@@ -16,11 +16,14 @@ export const TODO_EVENT_KINDS = Object.freeze([
16
16
  // 開発中に発見したplan跨ぎの依存を、active plan topologyの追記改変ではなく
17
17
  // version-boundなplan-scoped eventとして接続する。推定はせず、AIが発見した時だけ積む。
18
18
  'cross_plan_dependency',
19
+ // 誤って張った接続の除去(connectの対・2026-08-29オーナー裁定系)。対象eventのdigestを指す
20
+ // tombstoneとして積み、projectionが両方を読んで線を消す。歴史は書き換えない。
21
+ 'cross_plan_dependency_removed',
19
22
  ]);
20
23
 
21
24
  /** planへ帰属し、taskにもPhaseにも属さないevent kind。 */
22
25
  export const TODO_PLAN_SCOPED_EVENT_KINDS = Object.freeze([
23
- 'coordination_mode', 'cross_plan_dependency',
26
+ 'coordination_mode', 'cross_plan_dependency', 'cross_plan_dependency_removed',
24
27
  ]);
25
28
 
26
29
  /** 調整方式。witness=独立性を宣言し検証して並列する/conversation=会話で調整する。 */
@@ -524,6 +527,11 @@ function validPayload(event) {
524
527
  && dependencyRef(payload.from) && dependencyRef(payload.to)
525
528
  && nullableText(payload.reason) && payload.reason !== null;
526
529
  }
530
+ if (event.kind === 'cross_plan_dependency_removed') {
531
+ return exactRecord(payload, ['target_event_digest', 'reason'])
532
+ && isTodoDigest(payload.target_event_digest)
533
+ && nullableText(payload.reason) && payload.reason !== null;
534
+ }
527
535
  if (event.kind === 'start') return exactRecord(payload, ['override_reason']) && nullableText(payload.override_reason);
528
536
  if (event.kind === 'start_retracted') return exactRecord(payload, ['reason', 'target_start_digest'])
529
537
  && nullableText(payload.reason) && payload.reason !== null
@@ -407,9 +407,15 @@ export function projectTodoCoordination(events) {
407
407
  * 記録された線だけを返す。順序はtask identityで固定し、status/Ganttが同じ入力を見る。
408
408
  */
409
409
  export function projectTodoCrossPlanDependencies(members) {
410
+ // 除去tombstoneを先に集め、指された接続を線として数えない(歴史は両eventとも残る)
411
+ const removed = new Set();
412
+ for (const member of members) for (const event of member.plan_scoped?.events ?? []) {
413
+ if (event.kind === 'cross_plan_dependency_removed') removed.add(event.payload.target_event_digest);
414
+ }
410
415
  const dependencies = [];
411
416
  for (const member of members) for (const event of member.plan_scoped?.events ?? []) {
412
417
  if (event.kind !== 'cross_plan_dependency') continue;
418
+ if (removed.has(event.event_digest)) continue;
413
419
  dependencies.push({
414
420
  from: event.payload.from,
415
421
  to: event.payload.to,
@@ -2040,6 +2046,19 @@ export async function appendTodoEvent(options = {}) {
2040
2046
  if (options.event.kind === 'cross_plan_dependency') {
2041
2047
  validateCrossPlanDependencyTransition(store, member, options.event);
2042
2048
  }
2049
+ if (options.event.kind === 'cross_plan_dependency_removed') {
2050
+ const digest = options.event.payload.target_event_digest;
2051
+ const events = member.plan_scoped?.events ?? [];
2052
+ const target = events.find((entry) => entry.kind === 'cross_plan_dependency'
2053
+ && entry.event_digest === digest);
2054
+ if (target === undefined) {
2055
+ fail('DEPENDENCY_INVALID', 'cross_plan_dependency_not_found', { target_event_digest: digest });
2056
+ }
2057
+ if (events.some((entry) => entry.kind === 'cross_plan_dependency_removed'
2058
+ && entry.payload.target_event_digest === digest)) {
2059
+ fail('DEPENDENCY_INVALID', 'cross_plan_dependency_already_removed', { target_event_digest: digest });
2060
+ }
2061
+ }
2043
2062
  return appendPlanScopedEvent({
2044
2063
  repoRoot, member,
2045
2064
  input: { ...options.event, recorded_at: options.event.recorded_at ?? new Date().toISOString() },