kibi-cli 2.7.0 → 2.10.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.
Files changed (55) hide show
  1. package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -1
  2. package/dist/operations/bootstrap/intent-claims.js +24 -4
  3. package/dist/operations/bootstrap/markdown-candidates.d.ts +6 -0
  4. package/dist/operations/bootstrap/markdown-candidates.d.ts.map +1 -1
  5. package/dist/operations/bootstrap/markdown-candidates.js +54 -2
  6. package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
  7. package/dist/operations/bootstrap/presentation.js +6 -0
  8. package/dist/operations/bootstrap/requirement-claims.d.ts +18 -1
  9. package/dist/operations/bootstrap/requirement-claims.d.ts.map +1 -1
  10. package/dist/operations/bootstrap/requirement-claims.js +58 -0
  11. package/dist/operations/bootstrap/types.d.ts +6 -0
  12. package/dist/operations/bootstrap/types.d.ts.map +1 -1
  13. package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
  14. package/dist/operations/modeling/predicate-applyplan.js +1 -1
  15. package/dist/operations/modeling/predicate-types.d.ts +16 -1
  16. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  17. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  18. package/dist/operations/modeling/suggest-predicates.js +140 -14
  19. package/dist/operations/mutation/validation.d.ts.map +1 -1
  20. package/dist/operations/mutation/validation.js +30 -0
  21. package/dist/operations/mutation/workspace-mutation-lock.d.ts +22 -0
  22. package/dist/operations/mutation/workspace-mutation-lock.d.ts.map +1 -1
  23. package/dist/operations/mutation/workspace-mutation-lock.js +67 -1
  24. package/dist/operations/planning/apply-plan.d.ts +8 -0
  25. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  26. package/dist/operations/planning/apply-plan.js +217 -26
  27. package/dist/public/operations/catalog.d.ts +22 -2
  28. package/dist/public/operations/catalog.d.ts.map +1 -1
  29. package/dist/public/operations/contracts.d.ts.map +1 -1
  30. package/dist/public/operations/contracts.js +36 -22
  31. package/dist/public/operations/runtime-types.d.ts +12 -0
  32. package/dist/public/operations/runtime-types.d.ts.map +1 -1
  33. package/dist/public/operations/specs/bootstrap.d.ts +15 -0
  34. package/dist/public/operations/specs/bootstrap.d.ts.map +1 -1
  35. package/dist/public/operations/specs/bootstrap.js +15 -0
  36. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  37. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  38. package/dist/public/operations/specs/check-rules.generated.js +2 -1
  39. package/dist/public/operations/specs/check.d.ts +1 -1
  40. package/dist/public/operations/specs/planning.d.ts +7 -2
  41. package/dist/public/operations/specs/planning.d.ts.map +1 -1
  42. package/dist/public/operations/specs/planning.js +7 -2
  43. package/dist/public/skills/kibi-bootstrap/SKILL.md +6 -6
  44. package/dist/public/skills/kibi-usage/SKILL.md +14 -5
  45. package/dist/utils/rule-registry.generated.d.ts +5 -0
  46. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  47. package/dist/utils/rule-registry.generated.js +6 -0
  48. package/package.json +2 -2
  49. package/src/public/operations/contracts.ts +38 -22
  50. package/src/public/operations/runtime-types.ts +14 -0
  51. package/src/public/operations/specs/bootstrap.ts +18 -0
  52. package/src/public/operations/specs/check-rules.generated.ts +2 -1
  53. package/src/public/operations/specs/planning.ts +11 -2
  54. package/src/public/skills/kibi-bootstrap/SKILL.md +6 -6
  55. package/src/public/skills/kibi-usage/SKILL.md +14 -5
@@ -1,3 +1,5 @@
1
+ import { escapeAtom } from "../../prolog/codec.js";
2
+ import { parsePrologList } from "../mutation/serialization.js";
1
3
  import { analyzeSemanticAdvisorInput } from "../semantic-advisor/analyze-prose.js";
2
4
  import { semanticClaimKey } from "../semantic-advisor/clauses.js";
3
5
  import { composeOntologyCatalogForOperation } from "../semantic-advisor/plugin-orchestration.js";
@@ -143,6 +145,50 @@ function withMarginRejection(candidates) {
143
145
  };
144
146
  });
145
147
  }
148
+ const GROUNDING_RELATIONSHIP_TYPES = [
149
+ "requires_property",
150
+ "requires_predicate",
151
+ "requires_rule",
152
+ ];
153
+ function unquoted(value) {
154
+ return typeof value === "string" ? value.replace(/^['"]|['"]$/g, "") : "";
155
+ }
156
+ /**
157
+ * The logical grounding relationships an existing requirement already has for
158
+ * this claim. A modeled claim takes exactly one grounding relationship, so a
159
+ * second link (for example requires_predicate beside a bootstrap
160
+ * requires_property) fails the proposition-complete rule.
161
+ */
162
+ // implements REQ-model-predicates-grounding-aware-v2
163
+ async function existingClaimGrounding(prolog, requirementId, claimKey, warnings) {
164
+ if (prolog === null || !requirementId)
165
+ return [];
166
+ const found = [];
167
+ try {
168
+ for (const type of GROUNDING_RELATIONSHIP_TYPES) {
169
+ const targets = await prolog.query(`findall(To, kb_relationship(${type}, '${escapeAtom(requirementId)}', To), Targets)`);
170
+ if (!targets.success)
171
+ continue;
172
+ for (const target of parsePrologList(targets.bindings.Targets ?? "[]")) {
173
+ const fact = await prolog.query(`once((kb_entity('${escapeAtom(target)}', fact, _GroundProps), memberchk(claim_key=_GroundClaimRaw, _GroundProps), normalize_term_atom(_GroundClaimRaw, ClaimKey), (memberchk(fact_kind=_GroundKindRaw, _GroundProps) -> normalize_term_atom(_GroundKindRaw, FactKind) ; FactKind = '')))`);
174
+ if (!fact.success)
175
+ continue;
176
+ if (unquoted(fact.bindings.ClaimKey) !== claimKey)
177
+ continue;
178
+ found.push({
179
+ relationship: { type, from: requirementId, to: target },
180
+ factId: target,
181
+ factKind: unquoted(fact.bindings.FactKind) || null,
182
+ claimKey,
183
+ });
184
+ }
185
+ }
186
+ }
187
+ catch (error) {
188
+ warnings.push(`Existing grounding of ${requirementId} could not be read (${error instanceof Error ? error.message : String(error)}); check its requires_property, requires_predicate and requires_rule links before linking a predicate.`);
189
+ }
190
+ return found;
191
+ }
146
192
  // implements REQ-mcp-suggest-predicates
147
193
  export async function handleKbSuggestPredicates(prolog, args, context) {
148
194
  const text = normalizeText(args.text);
@@ -235,32 +281,72 @@ export async function handleKbSuggestPredicates(prolog, args, context) {
235
281
  }
236
282
  // Stage 3/4: bind only after eligibility, then decide conservatively.
237
283
  const unavailableSchema = Boolean(args.schemaId && selectedSchemas.length === 0);
284
+ const claimKey = semanticClaimKey(text);
285
+ const existingGrounding = await existingClaimGrounding(prolog, args.requirementId, claimKey, warnings);
286
+ const alreadyGrounded = existingGrounding.length > 0;
287
+ const completeCandidate = recommendedCandidate && recommendedCandidate.binding_status === "complete"
288
+ ? recommendedCandidate
289
+ : undefined;
290
+ // The action names the predicate state; existingGrounding (non-empty) is
291
+ // the separate "already grounded" signal. A complete candidate for a claim
292
+ // that is already grounded becomes a replacement, never a second link.
238
293
  const recommendedAction = !recommendedCandidate
239
294
  ? unavailableSchema
240
295
  ? "resolve_schema_reference"
241
296
  : "record_ontology_gap"
242
- : recommendedCandidate.binding_status === "complete"
243
- ? "apply_requires_predicate"
297
+ : completeCandidate
298
+ ? alreadyGrounded
299
+ ? "replace_grounding"
300
+ : "apply_requires_predicate"
244
301
  : "provide_argument_bindings";
245
- const applyPlan = recommendedCandidate && recommendedCandidate.binding_status === "complete"
246
- ? buildPredicateApplyPlan(recommendedCandidate, args)
302
+ const predicatePlan = completeCandidate
303
+ ? buildPredicateApplyPlan(completeCandidate, args)
304
+ : [];
305
+ // An already grounded claim gets no predicate write plan: a second
306
+ // grounding link would fail the proposition-complete rule, so the
307
+ // replacement plan below swaps the grounding instead. The ontology-gap
308
+ // observation is not a grounding relationship, so it is planned either way.
309
+ const applyPlan = completeCandidate
310
+ ? alreadyGrounded
311
+ ? []
312
+ : predicatePlan
247
313
  : !recommendedCandidate && !unavailableSchema
248
314
  ? buildGapApplyPlan(text, args)
249
315
  : [];
250
- const relationshipPlan = recommendedCandidate && recommendedCandidate.binding_status === "complete"
251
- ? buildRelationshipPlan(String(applyPlan[0]?.id ?? ""), args.requirementId, text, args.existingLogicClaims)
316
+ const plannedFactId = typeof predicatePlan[0]?.id === "string" ? predicatePlan[0].id : null;
317
+ const relationshipPlan = completeCandidate && !alreadyGrounded
318
+ ? buildRelationshipPlan(plannedFactId ?? "", args.requirementId, text, args.existingLogicClaims)
319
+ : null;
320
+ const replacementPlan = alreadyGrounded && plannedFactId !== null && args.requirementId
321
+ ? buildGroundingReplacementPlan(args.requirementId, plannedFactId, predicatePlan, existingGrounding)
322
+ : null;
323
+ const relationshipTarget = relationshipPlan !== null || replacementPlan !== null
324
+ ? plannedFactId
252
325
  : null;
326
+ const groundingSummary = existingGrounding
327
+ .map((row) => `${row.relationship.type} -> ${row.factId}`)
328
+ .join(", ");
329
+ if (alreadyGrounded) {
330
+ warnings.push(`${args.requirementId} already grounds this claim (${claimKey}) through ${groundingSummary}. A modeled claim takes exactly one logical grounding relationship, so adding requires_predicate beside it fails the proposition-complete rule. ${replacementPlan ? "Keep the existing grounding, or follow replacementPlan to swap it for the predicate." : "Keep the existing grounding; no predicate replacement is available yet."}`);
331
+ }
253
332
  const recommendedPredicateSchema = !recommendedCandidate && !unavailableSchema
254
333
  ? buildPredicateSchemaDraft(text, subject)
255
334
  : null;
256
- const textSummary = recommendedCandidate && recommendedCandidate.binding_status === "complete"
257
- ? `Suggested ${candidates.length} predicate candidate(s). Top applicable match: ${recommendedCandidate.predicate_name}. Apply structured predicate facts before falling back to prose.`
258
- : recommendedCandidate
259
- ? `Matched ${recommendedCandidate.predicate_name}, but exact reviewed values are still required for: ${recommendedCandidate.unbound_arguments.join(", ")}. No apply plan was generated.`
260
- : unavailableSchema
261
- ? `Requested predicate schema ${args.schemaId} is unavailable or semantically inapplicable. No apply plan was generated.`
262
- : "No predicate candidate passed the semantic applicability gate; record an ontology gap and review the generated schema draft instead of silently writing prose.";
263
- const claimKey = semanticClaimKey(text);
335
+ const groundedPrefix = alreadyGrounded
336
+ ? `${args.requirementId} already grounds this claim through ${groundingSummary}. `
337
+ : "";
338
+ const textSummary = groundedPrefix +
339
+ (replacementPlan
340
+ ? `To use ${completeCandidate?.predicate_name} instead, follow replacementPlan and link requires_predicate to ${plannedFactId}; otherwise keep the existing grounding.`
341
+ : completeCandidate
342
+ ? `Suggested ${candidates.length} predicate candidate(s). Top applicable match: ${completeCandidate.predicate_name}. Apply the predicate fact ${plannedFactId}, then link requires_predicate to that fact id (not a candidate id).`
343
+ : recommendedCandidate
344
+ ? `Matched ${recommendedCandidate.predicate_name}, but exact reviewed values are still required for: ${recommendedCandidate.unbound_arguments.join(", ")}. No apply plan was generated.`
345
+ : unavailableSchema
346
+ ? `Requested predicate schema ${args.schemaId} is unavailable or semantically inapplicable. No apply plan was generated.`
347
+ : alreadyGrounded
348
+ ? "No predicate candidate passed the semantic applicability gate; keep the existing grounding and record the ontology-gap observation in applyPlan so the missing schema stays visible."
349
+ : "No predicate candidate passed the semantic applicability gate; record an ontology gap and review the generated schema draft instead of silently writing prose.");
264
350
  const logicClaims = Array.from(new Set([...(args.existingLogicClaims ?? []), claimKey]));
265
351
  return {
266
352
  content: [{ type: "text", text: textSummary }],
@@ -276,11 +362,51 @@ export async function handleKbSuggestPredicates(prolog, args, context) {
276
362
  recommendedPredicateSchema,
277
363
  applyPlan,
278
364
  relationshipPlan,
365
+ relationshipTarget,
366
+ existingGrounding,
367
+ replacementPlan,
279
368
  warnings,
280
369
  },
281
370
  applyPlan,
282
371
  };
283
372
  }
373
+ /**
374
+ * Ordered steps that swap an existing grounding for the planned predicate
375
+ * fact while keeping one grounding relationship per modeled claim: write the
376
+ * fact, retract the old grounding link, then link requires_predicate.
377
+ */
378
+ // implements REQ-model-predicates-grounding-aware-v2
379
+ function buildGroundingReplacementPlan(requirementId, factId, predicatePlan, existing) {
380
+ return {
381
+ relationshipTarget: factId,
382
+ steps: [
383
+ {
384
+ operation: "kb_upsert",
385
+ input: predicatePlan[0],
386
+ reason: "Write the predicate fact that will ground the claim.",
387
+ },
388
+ {
389
+ operation: "kb_delete",
390
+ input: {
391
+ relationships: existing.map((row) => row.relationship),
392
+ },
393
+ reason: "Retract the existing grounding link so the claim keeps exactly one logical grounding relationship.",
394
+ },
395
+ {
396
+ operation: "kb_upsert",
397
+ input: {
398
+ type: "req",
399
+ id: requirementId,
400
+ relationships: [
401
+ { type: "requires_predicate", from: requirementId, to: factId },
402
+ ],
403
+ },
404
+ reason: `Link requires_predicate to ${factId}; upsert the requirement with its stored title and status and this relationship only.`,
405
+ },
406
+ ],
407
+ instructions: `Only replace the grounding when the predicate states the claim at least as precisely as ${existing.map((row) => row.factId).join(", ")}. Run kb_check after the last step.`,
408
+ };
409
+ }
284
410
  export async function executeSuggestPredicates(args, context) {
285
411
  return handleKbSuggestPredicates(context.prolog ?? null, args, context);
286
412
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/operations/mutation/validation.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,0BAA0B,EAC3B,MAAM,oCAAoC,CAAC;AAO5C,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA6F/D,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,IAAI,GACR,eAAe,CA4DjB;AAED,OAAO,EAAE,0BAA0B,EAAE,CAAC"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/operations/mutation/validation.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,0BAA0B,EAC3B,MAAM,oCAAoC,CAAC;AAO5C,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAqI/D,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,EAClB,GAAG,EAAE,IAAI,GACR,eAAe,CA4DjB;AAED,OAAO,EAAE,0BAA0B,EAAE,CAAC"}
@@ -48,6 +48,23 @@ const ALIASES = new Map([
48
48
  ["canonicalKey", "canonical_key"],
49
49
  ["closedWorld", "closed_world"],
50
50
  ]);
51
+ /** Property names agents use for entity prose that belongs in document.body. */
52
+ const DOCUMENT_TEXT_PROPERTIES = new Set([
53
+ "body",
54
+ "text",
55
+ "description",
56
+ "content",
57
+ "markdown",
58
+ "prose",
59
+ "summary",
60
+ "details",
61
+ ]);
62
+ /** Every top-level entity key the upsert entity schema does not define. */
63
+ function unknownEntityProperties(entity) {
64
+ const known = upsertEntitySchema.properties;
65
+ return Object.keys(entity).filter((key) => !Object.hasOwn(known, key));
66
+ }
67
+ // implements REQ-upsert-unknown-field-guidance
51
68
  function formatEntityErrors(entity, errors) {
52
69
  const messages = errors.map((error) => {
53
70
  const params = error.params;
@@ -61,6 +78,19 @@ function formatEntityErrors(entity, errors) {
61
78
  if (canonical) {
62
79
  return `${path}: unknown property '${property}'. Did you mean '${canonical}'? kb_upsert.properties uses snake_case typed fact fields.`;
63
80
  }
81
+ if (path === "root" && DOCUMENT_TEXT_PROPERTIES.has(property)) {
82
+ const others = unknownEntityProperties(entity).filter((key) => key !== property);
83
+ return `${path}: unknown property '${property}' (${error.message}). Entity prose (scenario, requirement, test, ADR or observation text) belongs in kb_upsert.document.body, not in kb_upsert.properties.${others.length > 0 ? ` Other unknown properties: ${others.map((key) => `'${key}'`).join(", ")}.` : ""}`;
84
+ }
85
+ if (path === "root") {
86
+ const unknown = unknownEntityProperties(entity);
87
+ const listed = unknown.length > 0 ? unknown : [property];
88
+ const proseHint = listed.some((key) => DOCUMENT_TEXT_PROPERTIES.has(key))
89
+ ? " Entity prose belongs in kb_upsert.document.body."
90
+ : "";
91
+ return `${path}: unknown propert${listed.length === 1 ? "y" : "ies"} ${listed.map((key) => `'${key}'`).join(", ")} (${error.message}). Remove ${listed.length === 1 ? "it" : "them"} from kb_upsert.properties or use fields the ${String(entity.type ?? "entity")} schema defines.${proseHint}`;
92
+ }
93
+ return `${path}: unknown property '${property}' (${error.message}).`;
64
94
  }
65
95
  if (error.keyword === "enum" && params.allowedValues) {
66
96
  return `${path}: ${error.message}. Allowed values: ${params.allowedValues.map(String).join(", ")}`;
@@ -9,6 +9,8 @@ export interface WorkspaceMutationLockFileSystem {
9
9
  }) => void;
10
10
  readonly unlinkSync: (target: string) => void;
11
11
  readonly rmdirSync: (target: string) => void;
12
+ /** Atomic rename; required to reclaim a lock from a dead holder. */
13
+ readonly renameSync?: (from: string, to: string) => void;
12
14
  }
13
15
  export interface WorkspaceMutationLockOptions {
14
16
  readonly lockFileRelative?: string;
@@ -17,6 +19,24 @@ export interface WorkspaceMutationLockOptions {
17
19
  readonly sleep?: (ms: number) => Promise<void>;
18
20
  readonly fileSystem?: WorkspaceMutationLockFileSystem;
19
21
  readonly isProcessAlive?: (pid: number) => boolean;
22
+ /**
23
+ * Decide whether a lock whose holder pid is verifiably not alive may be
24
+ * reclaimed instead of failing closed with operator recovery. Called only
25
+ * for a readable owner record whose pid is dead; a live, unverifiable,
26
+ * corrupt or missing owner always keeps the fail-closed refusal.
27
+ */
28
+ readonly reclaimDeadHolder?: (holder: DeadLockHolder) => boolean;
29
+ }
30
+ /** The owner record of a lock whose holder process is no longer alive. */
31
+ export interface DeadLockHolder {
32
+ readonly pid: number;
33
+ readonly token: string;
34
+ readonly acquiredAt: number;
35
+ }
36
+ /** A dead holder's lock this acquisition reclaimed. */
37
+ export interface WorkspaceMutationLockReclaim extends DeadLockHolder {
38
+ readonly reclaimedAt: number;
39
+ readonly reclaimedByPid: number;
20
40
  }
21
41
  export declare class WorkspaceMutationLockError extends Error {
22
42
  readonly name = "WorkspaceMutationLockError";
@@ -30,6 +50,8 @@ export interface WorkspaceMutationLockHandle {
30
50
  * removes a different owner token.
31
51
  */
32
52
  readonly release: () => void;
53
+ /** Dead holders this acquisition reclaimed the lock from, oldest first. */
54
+ readonly reclaimed?: readonly WorkspaceMutationLockReclaim[];
33
55
  }
34
56
  export declare function releaseWorkspaceMutationLock(handle: WorkspaceMutationLockHandle | undefined, operationFailure?: {
35
57
  readonly error: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-mutation-lock.d.ts","sourceRoot":"","sources":["../../../src/operations/mutation/workspace-mutation-lock.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,SAAS,EAAE,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,KACvC,IAAI,CAAC;IACV,QAAQ,CAAC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,QAAQ,CAAC,aAAa,EAAE,CACtB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;KAAE,KACxD,IAAI,CAAC;IACV,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAeD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,QAAQ,CAAC,UAAU,CAAC,EAAE,+BAA+B,CAAC;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACpD;AAGD,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAkB,IAAI,gCAAgC;CACvD;AAwMD,MAAM,WAAW,2BAA2B;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAGD,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,2BAA2B,GAAG,SAAS,EAC/C,gBAAgB,CAAC,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAC9C,SAAS,UAAQ,GAChB,IAAI,CAoBN;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,OAAO,GACjB,OAAO,CAWT;AAED;;;;GAIG;AAEH,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,4BAAiC,GACzC,OAAO,CAAC,2BAA2B,CAAC,CA4KtC;AAED;;;;;;;;GAQG;AAEH,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,OAAO,GAAE,4BAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAWZ"}
1
+ {"version":3,"file":"workspace-mutation-lock.d.ts","sourceRoot":"","sources":["../../../src/operations/mutation/workspace-mutation-lock.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { mkdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { mkdirSync, readFileSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { InputError, OperationError } from "../../cli-errors.js";
4
4
  /**
@@ -23,6 +23,7 @@ const NODE_FILE_SYSTEM = {
23
23
  },
24
24
  unlinkSync: (target) => unlinkSync(target),
25
25
  rmdirSync: (target) => rmdirSync(target),
26
+ renameSync: (from, to) => renameSync(from, to),
26
27
  };
27
28
  // implements REQ-generated-coordinate-persistence
28
29
  export class WorkspaceMutationLockError extends Error {
@@ -156,8 +157,52 @@ function observeExistingLock(fileSystem, target, ownerPath, isProcessAlive) {
156
157
  return {
157
158
  kind: "recovery",
158
159
  description: `holder pid ${record.pid} is not live`,
160
+ // Legacy file locks are never reclaimed automatically.
161
+ ...(source === "lock owner metadata" ? { deadHolder: record } : {}),
159
162
  };
160
163
  }
164
+ /**
165
+ * Move a dead holder's lock directory aside atomically, confirm the moved
166
+ * owner is still that dead holder, then delete it. A concurrent reclaimer
167
+ * loses the rename and simply retries acquisition. If another writer replaced
168
+ * the lock between observation and rename, the lock is put back and the
169
+ * acquisition fails closed.
170
+ */
171
+ function reclaimDeadLock(fileSystem, target, holder, token) {
172
+ const rename = fileSystem.renameSync;
173
+ if (rename === undefined)
174
+ return "changed";
175
+ const tombstone = `${target}.reclaimed-${token}`;
176
+ try {
177
+ rename(target, tombstone);
178
+ }
179
+ catch (error) {
180
+ const code = errorCode(error);
181
+ if (code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY")
182
+ return "changed";
183
+ throw new WorkspaceMutationLockError(`failed to reclaim workspace mutation lock at ${target} from dead pid ${holder.pid}: ${errorDetail(error)}`);
184
+ }
185
+ const tombstoneOwner = path.join(tombstone, OWNER_FILE);
186
+ let moved = null;
187
+ try {
188
+ moved = readLockRecord(fileSystem.readFileSync(tombstoneOwner));
189
+ }
190
+ catch {
191
+ moved = null;
192
+ }
193
+ if (moved === null || moved.token !== holder.token) {
194
+ try {
195
+ rename(tombstone, target);
196
+ }
197
+ catch {
198
+ // The original path was taken again; the tombstone stays for an operator.
199
+ }
200
+ throw new OperationError("SOURCE_MUTATION_LOCK_RECOVERY_REQUIRED", `workspace mutation lock at ${target} changed owner while it was reclaimed from dead pid ${holder.pid}. Quiesce all Kibi and source-mutating writers, then verify ${target} and ${tombstone} before removing either`, false);
201
+ }
202
+ removeLockOwner(fileSystem, tombstone, tombstoneOwner, true);
203
+ removeLockDirectory(fileSystem, tombstone);
204
+ return "reclaimed";
205
+ }
161
206
  function defaultIsProcessAlive(pid) {
162
207
  try {
163
208
  process.kill(pid, 0);
@@ -228,6 +273,7 @@ export async function acquireWorkspaceMutationLock(workspaceRoot, options = {})
228
273
  throw new WorkspaceMutationLockError(`failed to create workspace mutation lock parent at ${parentPath} after ${parentResult.attempts} attempt(s): ${errorDetail(parentResult.error)}`);
229
274
  }
230
275
  let acquired = false;
276
+ const reclaimed = [];
231
277
  let lastBlocker = "unknown";
232
278
  let lastObservationMissing = false;
233
279
  let missingOwnerRereads = 0;
@@ -257,6 +303,25 @@ export async function acquireWorkspaceMutationLock(workspaceRoot, options = {})
257
303
  }
258
304
  throw new OperationError("SOURCE_MUTATION_LOCK_RECOVERY_REQUIRED", `workspace mutation lock at ${target} requires operator recovery: ${observation.description} after ${missingOwnerRereads} reread(s). Quiesce all Kibi and source-mutating writers, then manually verify the owner metadata and remove the lock only after confirming no operation is active`, false);
259
305
  }
306
+ if (observation.kind === "recovery" &&
307
+ observation.deadHolder !== undefined &&
308
+ fileSystem.renameSync !== undefined &&
309
+ options.reclaimDeadHolder?.(observation.deadHolder) === true) {
310
+ const holder = observation.deadHolder;
311
+ if (reclaimDeadLock(fileSystem, target, holder, token) === "reclaimed") {
312
+ reclaimed.push({
313
+ pid: holder.pid,
314
+ token: holder.token,
315
+ acquiredAt: holder.acquiredAt,
316
+ reclaimedAt: now(),
317
+ reclaimedByPid: process.pid,
318
+ });
319
+ }
320
+ else {
321
+ await sleep(RETRY_INTERVAL_MS);
322
+ }
323
+ continue;
324
+ }
260
325
  if (observation.kind === "recovery") {
261
326
  throw new OperationError("SOURCE_MUTATION_LOCK_RECOVERY_REQUIRED", `workspace mutation lock at ${target} requires operator recovery: ${observation.description}. Quiesce all Kibi and source-mutating writers, then manually verify the owner metadata and remove the lock only after confirming no operation is active`, false);
262
327
  }
@@ -312,6 +377,7 @@ export async function acquireWorkspaceMutationLock(workspaceRoot, options = {})
312
377
  }
313
378
  let releaseState = "owned";
314
379
  return {
380
+ ...(reclaimed.length > 0 ? { reclaimed } : {}),
315
381
  release: () => {
316
382
  if (releaseState === "released")
317
383
  return;
@@ -9,6 +9,14 @@ export declare function assertBootstrapRecoveryDependencies(remaining: readonly
9
9
  dependsOn?: readonly string[];
10
10
  }[], applied: ReadonlySet<string>): void;
11
11
  export declare function orderBootstrapActions(actions: readonly BootstrapAction[], completed?: Set<string>): BootstrapAction[];
12
+ /**
13
+ * Checks a plan-bearing kb_apply_plan call can start: the approved hash, the
14
+ * plan shape and canonical hash, and for a bootstrap plan its live snapshots.
15
+ * Writes nothing. An MCP async apply runs it before returning the job
16
+ * receipt, so input and stale-plan errors fail the call instead of the job.
17
+ * The apply itself repeats every check under the workspace lock.
18
+ */
19
+ export declare function preflightApplyPlan(args: ApplyPlanArgs, context: OperationContext): Promise<void>;
12
20
  export declare function executeApplyPlan(args: ApplyPlanArgs, context: OperationContext): Promise<{
13
21
  content: Array<{
14
22
  type: "text";
@@ -1 +1 @@
1
- {"version":3,"file":"apply-plan.d.ts","sourceRoot":"","sources":["../../../src/operations/planning/apply-plan.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAEV,gBAAgB,EAGjB,MAAM,0CAA0C,CAAC;AAQlD,OAAO,EACL,KAAK,eAAe,EAIrB,MAAM,uBAAuB,CAAC;AA8D/B,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EAGhB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,aAAa,EACb,eAAe,EACf,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,eAAO,MAAM,yBAAyB,EAAG,2BAAoC,CAAC;AA6G9E,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,IAAI,CAMN;AAED,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,EACnE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,GAC3B,IAAI,CAaN;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,SAAS,cAAoB,GAC5B,eAAe,EAAE,CA2CnB;AA6xED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC;IACT,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,iBAAiB,EAAE,eAAe,CAAC;CACpC,CAAC,CA6BD"}
1
+ {"version":3,"file":"apply-plan.d.ts","sourceRoot":"","sources":["../../../src/operations/planning/apply-plan.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAEV,gBAAgB,EAGjB,MAAM,0CAA0C,CAAC;AAQlD,OAAO,EACL,KAAK,eAAe,EAIrB,MAAM,uBAAuB,CAAC;AA+D/B,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EAGhB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,aAAa,EACb,eAAe,EACf,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,eAAO,MAAM,yBAAyB,EAAG,2BAAoC,CAAC;AA6G9E,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,IAAI,CAMN;AAED,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,EACnE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,GAC3B,IAAI,CAaN;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS,eAAe,EAAE,EACnC,SAAS,cAAoB,GAC5B,eAAe,EAAE,CA2CnB;AA8zBD;;;;;;GAMG;AAEH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CAiCf;AAyqDD,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC;IACT,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,iBAAiB,EAAE,eAAe,CAAC;CACpC,CAAC,CAkDD"}