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.
- package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -1
- package/dist/operations/bootstrap/intent-claims.js +24 -4
- package/dist/operations/bootstrap/markdown-candidates.d.ts +6 -0
- package/dist/operations/bootstrap/markdown-candidates.d.ts.map +1 -1
- package/dist/operations/bootstrap/markdown-candidates.js +54 -2
- package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
- package/dist/operations/bootstrap/presentation.js +6 -0
- package/dist/operations/bootstrap/requirement-claims.d.ts +18 -1
- package/dist/operations/bootstrap/requirement-claims.d.ts.map +1 -1
- package/dist/operations/bootstrap/requirement-claims.js +58 -0
- package/dist/operations/bootstrap/types.d.ts +6 -0
- package/dist/operations/bootstrap/types.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.js +1 -1
- package/dist/operations/modeling/predicate-types.d.ts +16 -1
- package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +140 -14
- package/dist/operations/mutation/validation.d.ts.map +1 -1
- package/dist/operations/mutation/validation.js +30 -0
- package/dist/operations/mutation/workspace-mutation-lock.d.ts +22 -0
- package/dist/operations/mutation/workspace-mutation-lock.d.ts.map +1 -1
- package/dist/operations/mutation/workspace-mutation-lock.js +67 -1
- package/dist/operations/planning/apply-plan.d.ts +8 -0
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +217 -26
- package/dist/public/operations/catalog.d.ts +22 -2
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +36 -22
- package/dist/public/operations/runtime-types.d.ts +12 -0
- package/dist/public/operations/runtime-types.d.ts.map +1 -1
- package/dist/public/operations/specs/bootstrap.d.ts +15 -0
- package/dist/public/operations/specs/bootstrap.d.ts.map +1 -1
- package/dist/public/operations/specs/bootstrap.js +15 -0
- package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
- package/dist/public/operations/specs/check-rules.generated.js +2 -1
- package/dist/public/operations/specs/check.d.ts +1 -1
- package/dist/public/operations/specs/planning.d.ts +7 -2
- package/dist/public/operations/specs/planning.d.ts.map +1 -1
- package/dist/public/operations/specs/planning.js +7 -2
- package/dist/public/skills/kibi-bootstrap/SKILL.md +6 -6
- package/dist/public/skills/kibi-usage/SKILL.md +14 -5
- package/dist/utils/rule-registry.generated.d.ts +5 -0
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +6 -0
- package/package.json +2 -2
- package/src/public/operations/contracts.ts +38 -22
- package/src/public/operations/runtime-types.ts +14 -0
- package/src/public/operations/specs/bootstrap.ts +18 -0
- package/src/public/operations/specs/check-rules.generated.ts +2 -1
- package/src/public/operations/specs/planning.ts +11 -2
- package/src/public/skills/kibi-bootstrap/SKILL.md +6 -6
- 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
|
-
:
|
|
243
|
-
?
|
|
297
|
+
: completeCandidate
|
|
298
|
+
? alreadyGrounded
|
|
299
|
+
? "replace_grounding"
|
|
300
|
+
: "apply_requires_predicate"
|
|
244
301
|
: "provide_argument_bindings";
|
|
245
|
-
const
|
|
246
|
-
? buildPredicateApplyPlan(
|
|
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
|
|
251
|
-
|
|
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
|
|
257
|
-
?
|
|
258
|
-
:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
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":"
|
|
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;
|
|
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"}
|