@quolu/lattice 0.62.2 → 0.63.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/README.ja.md CHANGED
@@ -228,6 +228,10 @@ lattice todo start --plan <key> --task <id> --parallel-frontier
228
228
  lattice todo start --plan <key> --task <id> --override-reason <reason>
229
229
  ```
230
230
 
231
+ 記録があるのに対象工程が未宣言・失効なら`todo start`は`INDEPENDENCE_UNVERIFIED`で拒否します。
232
+ `independence compile`は`next_ready`がwitnessに無いとき`INDEPENDENCE_READY_UNDECLARED`で拒否します。
233
+ remainingのA工程は同じwitnessへ含めます。競合の同時起動は助言のままです。
234
+
231
235
  `--serial-confirmed`と`--serialization-reviewed`は古い手順の互換として受理しますが、
232
236
  再実行の条件ではありません。直列理由の文言を審査して拒否することもありません。
233
237
 
package/README.md CHANGED
@@ -193,6 +193,9 @@ lattice todo seam-proposal land --plan <key> --names names.json
193
193
  parallel set. Starting one of them does not require `--parallel-frontier` or
194
194
  `--override-reason`. Those flags record intent; they are not gates.
195
195
  `--serial-confirmed` and `--serialization-reviewed` are accepted only for compatibility.
196
+ If an independence record exists but the task is undeclared or stale, `todo start`
197
+ fails with `INDEPENDENCE_UNVERIFIED`. `independence compile` fails with
198
+ `INDEPENDENCE_READY_UNDECLARED` when `next_ready` is missing from the witness.
196
199
 
197
200
  ```bash
198
201
  lattice todo start --plan <key> --task <id>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.62.2",
3
+ "version": "0.63.0",
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
@@ -156,7 +156,9 @@ ${TODO_INDEPENDENCE_WORKFLOW.join('\n')}
156
156
 
157
157
  判定していない工程は「競合が無い」ではなく「未検査」として扱われ、
158
158
  todo startのadvisoryとtodo independenceの投影が、その状況と次の一歩を返す。
159
- 並列既定はstatusのdispatch_frontierとnext_actionが案内する。着手・作成を拒まない。
159
+ 記録があるのに対象工程が未宣言・失効なら todo start は拒否する。
160
+ next_ready が witness に無い independence compile も拒否する。
161
+ 競合の同時起動は助言のまま。並列既定はstatusのdispatch_frontierとnext_actionが案内する。
160
162
  `,
161
163
  sensor: `Usage: lattice sensor <init|sync> [path] --json
162
164
  lattice sensor diff <rootA> <rootB> [options] --json
@@ -990,9 +990,24 @@ export async function attachPullWorker({ runDir, taskId, input, environment = pr
990
990
  const occupied = state.intakes.find((entry) => (
991
991
  entry.task_id !== taskId && entry.accepted === null && entry.worker?.pid === input.pid
992
992
  ));
993
- if (occupied) {
993
+ if (occupied && occupied.intervention.state !== 'hold') {
994
994
  fail('WORKER_ALREADY_ATTACHED', `同じworkerは複数active intakeへattachできない: ${occupied.task_id}`);
995
995
  }
996
+ if (occupied && occupied.intervention.state === 'hold') {
997
+ if (occupied.worker.stopped) {
998
+ await signalAttachedWorker(occupied, 'SIGCONT');
999
+ current = await appendEvent(runDir, current, buildEvent({
1000
+ events: current.events, meta: current.meta, kind: 'worker_resumed',
1001
+ taskId: occupied.task_id, payload: { released_by: 'stale_hold_reattach' },
1002
+ }));
1003
+ }
1004
+ current = await appendEvent(runDir, current, buildEvent({
1005
+ events: current.events, meta: current.meta, kind: 'worker_detached',
1006
+ taskId: occupied.task_id,
1007
+ payload: { detached_by: actor, pid: occupied.worker.pid, resumed: occupied.worker.stopped },
1008
+ }));
1009
+ state = project(current.events, current.meta);
1010
+ }
996
1011
  if (intake.worker !== null) {
997
1012
  if (digestArtifact(binding) !== digestArtifact({
998
1013
  pid: intake.worker.pid, process_group_id: intake.worker.process_group_id,
@@ -1082,6 +1097,11 @@ export async function detachPullWorker({ runDir, taskId, environment = process.e
1082
1097
  } finally { await lock.release(); }
1083
1098
  }
1084
1099
 
1100
+ export function concurrentAttachBlocked(occupied) {
1101
+ if (!occupied) return false;
1102
+ return occupied.intervention?.state !== 'hold';
1103
+ }
1104
+
1085
1105
  export function remainingRuntimeConflictFindings(findings, acceptedTaskId) {
1086
1106
  if (!Array.isArray(findings)) return [];
1087
1107
  return findings.filter((finding) => !(finding.todo_ids ?? []).includes(acceptedTaskId));
package/src/todo-cli.mjs CHANGED
@@ -133,8 +133,10 @@ import {
133
133
  compileTodoIndependence,
134
134
  migrateWitnessSetTaskIds,
135
135
  projectIndependenceFrontier,
136
+ TodoIndependenceError,
136
137
  } from './todo-independence.mjs';
137
138
  import {
139
+ independenceStartRefusal,
138
140
  selectIndependenceGuidance,
139
141
  selectWitnessScaffoldGuidance,
140
142
  selectSeamProposalGuidance,
@@ -728,8 +730,9 @@ function designMemoProjection(task) {
728
730
  /**
729
731
  * 着手しようとしているtaskについて、記録済みの独立性から助言を組む(ADR 0128 Decision 5)。
730
732
  *
731
- * 助言であって拒否ではない。ADR 0063のdispatch契約は変えない。ただし助言を計算できない
732
- * 状況——git HEADが読めない等——はsilent degradeせず、呼び出し側でstart自体を止める。
733
+ * どの ready を取るかは助言のまま。記録があるのに対象工程が未検証なら呼び出し側が
734
+ * start を拒否する(ADR 0182)。助言を計算できない状況——git HEADが読めない等——は
735
+ * silent degradeせず、呼び出し側でstart自体を止める。
733
736
  */
734
737
  async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
735
738
  const artifact = await readTodoIndependenceArtifact({ repoRoot, store, planKey });
@@ -861,6 +864,14 @@ async function startTask({
861
864
  const advisory = await startAdvisory({
862
865
  repoRoot, store, projection, planKey, taskId: resolvedTaskId,
863
866
  });
867
+ const refusal = independenceStartRefusal(advisory.guidance);
868
+ if (refusal !== null) {
869
+ throw new TodoStoreError('INDEPENDENCE_UNVERIFIED', refusal.code, refusal.message, {
870
+ plan_key: planKey,
871
+ task_id: resolvedTaskId,
872
+ next_action: refusal.next_action,
873
+ });
874
+ }
864
875
  const structureContext = await startStructureContext({
865
876
  repoRoot, store, planKey, taskId: resolvedTaskId,
866
877
  });
@@ -2388,9 +2399,41 @@ async function structure({ repoRoot, requestedPlanKey }) {
2388
2399
 
2389
2400
  async function independenceCompile({ repoRoot, planKey, inputRef }) {
2390
2401
  const witnessSet = await readWitnessSetInput(repoRoot, inputRef);
2402
+ const storeForReady = await readTodoStore({ repoRoot });
2403
+ const memberForReady = storeForReady.members.find(({ descriptor }) => descriptor.plan_key === planKey);
2404
+ if (!memberForReady) {
2405
+ throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, { plan_key: planKey });
2406
+ }
2407
+ const planTaskIds = new Set(memberForReady.plan.tasks.map(({ task_id: taskId }) => taskId));
2408
+ const declaredIds = Object.keys(witnessSet.manual_witness ?? {});
2409
+ const absentFromPlan = declaredIds.filter((taskId) => !planTaskIds.has(taskId)).sort();
2410
+ if (absentFromPlan.length > 0) {
2411
+ throw new TodoIndependenceError(
2412
+ 'INDEPENDENCE_TASK_ABSENT',
2413
+ 'witness_task_absent_from_plan',
2414
+ { task_ids: absentFromPlan },
2415
+ );
2416
+ }
2417
+ const readyIds = projectTodoStatus(storeForReady, TODO_STATUS_DISPATCH_ONLY).next_ready
2418
+ .filter((task) => task.plan_key === planKey)
2419
+ .map((task) => task.task_id);
2420
+ const declared = new Set(declaredIds);
2421
+ const missingReady = readyIds.filter((taskId) => !declared.has(taskId)).sort();
2422
+ if (missingReady.length > 0) {
2423
+ throw new TodoStoreError(
2424
+ 'INDEPENDENCE_READY_UNDECLARED',
2425
+ 'ready_tasks_missing_from_witness',
2426
+ undefined,
2427
+ {
2428
+ plan_key: planKey,
2429
+ missing_task_ids: missingReady,
2430
+ next_action: 'add_task_to_witness_set_then_compile',
2431
+ },
2432
+ );
2433
+ }
2391
2434
  const observation = await collectTodoIndependenceAuthoritativeObservation({ repoRoot, witnessSet });
2392
2435
  const baseSha = observation.head_sha;
2393
- const store = await readTodoStore({ repoRoot });
2436
+ const store = storeForReady;
2394
2437
  const member = store.members.find(({ descriptor }) => descriptor.plan_key === planKey);
2395
2438
  if (!member) throw new TodoStoreError('STORE_INCONSISTENT', 'plan_not_active', undefined, { plan_key: planKey });
2396
2439
 
@@ -4,8 +4,10 @@
4
4
  * 状況から`{code, message, next_action}`を引く単一正本。advisory・投影・typed error・helpは
5
5
  * すべてここから引く。面ごとに文言を書けば必ずずれ、同じ状況に別の説明が付く。
6
6
  *
7
- * 文は事実と次の一歩だけを述べ、指示しない。dispatchの意思決定はhostが所有する
7
+ * 文は事実と次の一歩だけを述べ、指示しない。どのreadyを取るかはhostが所有する
8
8
  * (ADR 0063 Decision 5)。命令形にするとLatticeがagentを統制する面へ滑る。
9
+ * ただし記録があるのに対象工程が未検証なら start は拒否する(ADR 0182)。
10
+ * それは配車ではなく、intakeが hold する着手を journal へ書かない。
9
11
  */
10
12
 
11
13
  export const TODO_INDEPENDENCE_GUIDANCE_CODES = Object.freeze([
@@ -81,7 +83,7 @@ const CATALOG = Object.freeze({
81
83
  next_action: 'declare_witness_set_then_compile',
82
84
  }),
83
85
  independence_task_undeclared: Object.freeze({
84
- message: 'この工程はwitness setで宣言されていないため、記録には含まれていない。',
86
+ message: 'この工程はwitness setで宣言されていないため、記録には含まれていない。startは拒否する。',
85
87
  next_action: 'add_task_to_witness_set_then_compile',
86
88
  }),
87
89
  independence_contract_superseded: Object.freeze({
@@ -173,6 +175,27 @@ const SEVERABILITY_HINT = Object.freeze({
173
175
  serial: '共有stateまたはeffectの衝突なので、分割では切り離せない。',
174
176
  });
175
177
 
178
+ /**
179
+ * 記録があるのに対象工程が未検証なら start を拒否する code。
180
+ * 記録が無い(independence_unrecorded)は従来どおり助言だけで通す。
181
+ * 競合(conflict_*)はどの ready を取るかの dispatch なので拒否しない。
182
+ */
183
+ export const INDEPENDENCE_START_REFUSAL_CODES = Object.freeze([
184
+ 'independence_task_undeclared',
185
+ 'independence_stale_for_task',
186
+ 'independence_superseded',
187
+ 'independence_contract_superseded',
188
+ 'independence_verdicts_absent',
189
+ ]);
190
+
191
+ export function independenceStartRefusal(guidance) {
192
+ if (guidance === null || typeof guidance !== 'object' || Array.isArray(guidance)) {
193
+ throw new TypeError('independence start refusal guidance is invalid');
194
+ }
195
+ if (!INDEPENDENCE_START_REFUSAL_CODES.includes(guidance.code)) return null;
196
+ return guidance;
197
+ }
198
+
176
199
  export function todoIndependenceGuidance(code, { severability = null } = {}) {
177
200
  const entry = CATALOG[code];
178
201
  if (entry === undefined) {
@@ -305,9 +328,11 @@ export function selectSeamProposalGuidance({ coverage, unknownKinds = [] }) {
305
328
  export const TODO_INDEPENDENCE_WORKFLOW = Object.freeze([
306
329
  '1. 宣言する: .lattice/todo/witness/<plan_key>.json へ、ToDoごとのowns/reads/writes/affected_testsを書く',
307
330
  ' 係争資源しか所有していないToDoは、ownsまたはwritesの内側で自分が触るsymbolをconcern_anchorsへ宣言できる(witness set v2)。並列可否の判定には写らず、切断候補の束縛だけに効く',
308
- '2. 判定する: lattice todo independence compile --plan <key> --input <ref>(実sensorを引く。pretty-printやdigest未計算の下書きは機械が直す)',
331
+ ' remaining A 工程(まだ done でない ready / blocked)も同じ witness に含める。現在の ready だけを compile すると、次の frontier start が INDEPENDENCE_UNVERIFIED になる',
332
+ '2. 判定する: lattice todo independence compile --plan <key> --input <ref>(実sensorを引く。pretty-printやdigest未計算の下書きは機械が直す)。next_ready が witness に無い compile は拒否する',
309
333
  '3. 読む: lattice todo independence --plan <key> --json(sensorを引かず、記録とHEAD照合だけで返る)',
310
334
  '4. 追従する: plan改訂後は lattice todo independence witness migrate --plan <key> で宣言を写してから再compileする',
335
+ '5. 記録があるのに対象工程が未宣言・失効なら todo start は拒否する。競合の同時起動は助言のまま',
311
336
  ]);
312
337
 
313
338
  /**