kibi-runtime 2.5.0 → 2.7.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/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * intentionally explicit: adding a CLI module does not silently widen the
5
5
  * runtime package contract.
6
6
  */
7
- export type { Clock, OperationContext, OperationPlugins, OperationRuntime, RuntimeOptions, RuntimeOperationSpec, PrologPort, PrologQueryResult, FilesystemPort, GitPort, NetworkPort, EngineCommandV1 as CliEngineCommandV1, EnginePort as CliEnginePort, } from "kibi-cli/operations/runtime-types";
7
+ export type { Clock, OperationContext, OperationPlugins, OperationRuntime, OperationProgress, ProgressReporter, RuntimeOptions, RuntimeOperationSpec, PrologPort, PrologQueryResult, FilesystemPort, GitPort, NetworkPort, EngineCommandV1 as CliEngineCommandV1, EnginePort as CliEnginePort, } from "kibi-cli/operations/runtime-types";
8
8
  export type { EngineCommandV1, EnginePort } from "./engine.js";
9
9
  export { executeOperation } from "kibi-cli/operations/runtime-types";
10
10
  export type { KibiResult, KibiResultStatus, OperationContent, OperationEffect, OperationName, OperationResult, OperationSpec, } from "kibi-cli/operations";
package/dist/index.js CHANGED
@@ -563,12 +563,20 @@ var init_contracts = __esm(() => {
563
563
  "apply_requires_predicate",
564
564
  "provide_argument_bindings",
565
565
  "resolve_schema_reference",
566
- "record_ontology_gap"
566
+ "record_ontology_gap",
567
+ "review_nonlogical",
568
+ "already_grounded"
567
569
  ]
568
570
  },
569
571
  recommendedPredicateSchema: { type: ["object", "null"] },
570
572
  applyPlan: recordArray,
571
573
  relationshipPlan: { type: ["object", "null"] },
574
+ relationshipTarget: {
575
+ type: ["string", "null"],
576
+ description: "The planned predicate fact id (FACT-PRED-...) that a requires_predicate link must target. Candidate ids (SUGGEST-...) are never relationship targets."
577
+ },
578
+ existingGrounding: recordArray,
579
+ replacementPlan: { type: ["object", "null"] },
572
580
  warnings: stringArray
573
581
  }),
574
582
  kb_plan_bootstrap: objectData({
@@ -703,26 +711,32 @@ var init_contracts = __esm(() => {
703
711
  diagnostics: stringArray,
704
712
  capabilityPlugins: recordValue
705
713
  }),
706
- kb_apply_plan: objectData({
707
- version: stringValue,
708
- outcome: stringValue,
709
- planHash: stringValue,
710
- changedEntities: integerValue,
711
- changedRelationships: integerValue,
712
- changedPaths: stringArray,
713
- finalSnapshots: recordValue,
714
- validationSummary: recordValue,
715
- recoveryJournalId: { type: ["string", "null"] },
716
- deleted: integerValue,
717
- sourcePaths: stringArray,
718
- actionResults: recordArray,
719
- notes: stringArray,
720
- remainingPlan: recordValue,
721
- closeout: recordValue,
722
- status: stringValue,
723
- effectFailures: recordArray,
724
- nextActions: recordArray
725
- }),
714
+ kb_apply_plan: {
715
+ description: "Synchronous apply result, or a kibi.job.v1 receipt when async:true detached the apply into a background job (poll kb_job_status with the jobId).",
716
+ anyOf: [
717
+ objectData({
718
+ version: stringValue,
719
+ outcome: stringValue,
720
+ planHash: stringValue,
721
+ changedEntities: integerValue,
722
+ changedRelationships: integerValue,
723
+ changedPaths: stringArray,
724
+ finalSnapshots: recordValue,
725
+ validationSummary: recordValue,
726
+ recoveryJournalId: { type: ["string", "null"] },
727
+ deleted: integerValue,
728
+ sourcePaths: stringArray,
729
+ actionResults: recordArray,
730
+ notes: stringArray,
731
+ remainingPlan: recordValue,
732
+ closeout: recordValue,
733
+ status: stringValue,
734
+ effectFailures: recordArray,
735
+ nextActions: recordArray
736
+ }),
737
+ jobReceiptData
738
+ ]
739
+ },
726
740
  kb_ingest_proof: objectData({
727
741
  artifactDigest: stringValue,
728
742
  environmentHash: stringValue,
@@ -23292,6 +23306,12 @@ var init_rule_registry_generated = __esm(() => {
23292
23306
  enforcementClass: "migration",
23293
23307
  category: "integrity"
23294
23308
  },
23309
+ {
23310
+ name: "related-requirement-unmodeled",
23311
+ description: "A current requirement with unresolved (missing) propositions that relates_to a current requirement modeled with strict property or ground predicate facts; its claims are never compared by domain-contradictions until they are modeled against the same subject or predicates, or it supersedes that requirement",
23312
+ enforcementClass: "canonical",
23313
+ category: "integrity"
23314
+ },
23295
23315
  {
23296
23316
  name: "proof-contract-symbols",
23297
23317
  description: "Detect unresolved required_proofs.symbol_id values, type-shape required proofs, and proof_bindings.source_file disagreement with the named symbol sourceFile",
@@ -29834,6 +29854,10 @@ function relationshipSchemaValidator() {
29834
29854
  relationshipValidator ??= ajv2.compile(relationship_default);
29835
29855
  return relationshipValidator;
29836
29856
  }
29857
+ function unknownEntityProperties(entity) {
29858
+ const known = upsertEntitySchema.properties;
29859
+ return Object.keys(entity).filter((key) => !Object.hasOwn(known, key));
29860
+ }
29837
29861
  function formatEntityErrors(entity, errors) {
29838
29862
  const messages = errors.map((error) => {
29839
29863
  const params = error.params;
@@ -29847,6 +29871,17 @@ function formatEntityErrors(entity, errors) {
29847
29871
  if (canonical) {
29848
29872
  return `${path}: unknown property '${property}'. Did you mean '${canonical}'? kb_upsert.properties uses snake_case typed fact fields.`;
29849
29873
  }
29874
+ if (path === "root" && DOCUMENT_TEXT_PROPERTIES.has(property)) {
29875
+ const others = unknownEntityProperties(entity).filter((key) => key !== property);
29876
+ 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(", ")}.` : ""}`;
29877
+ }
29878
+ if (path === "root") {
29879
+ const unknown = unknownEntityProperties(entity);
29880
+ const listed = unknown.length > 0 ? unknown : [property];
29881
+ const proseHint = listed.some((key) => DOCUMENT_TEXT_PROPERTIES.has(key)) ? " Entity prose belongs in kb_upsert.document.body." : "";
29882
+ 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}`;
29883
+ }
29884
+ return `${path}: unknown property '${property}' (${error.message}).`;
29850
29885
  }
29851
29886
  if (error.keyword === "enum" && params.allowedValues) {
29852
29887
  return `${path}: ${error.message}. Allowed values: ${params.allowedValues.map(String).join(", ")}`;
@@ -29931,7 +29966,7 @@ function validateProofExemptionPairing(entity) {
29931
29966
  throw new Error("Entity validation failed: proof_exempt requires a non-empty proof_exempt_reason explaining why the requirement is outside E2E-proof scope");
29932
29967
  }
29933
29968
  }
29934
- var import_ajv2, ajv2, schema2, properties, entityValidator, relationshipValidator, upsertEntitySchema, ALIASES;
29969
+ var import_ajv2, ajv2, schema2, properties, entityValidator, relationshipValidator, upsertEntitySchema, ALIASES, DOCUMENT_TEXT_PROPERTIES;
29935
29970
  var init_validation = __esm(() => {
29936
29971
  init_proof_receipt();
29937
29972
  init_entity();
@@ -29968,6 +30003,16 @@ var init_validation = __esm(() => {
29968
30003
  ["canonicalKey", "canonical_key"],
29969
30004
  ["closedWorld", "closed_world"]
29970
30005
  ]);
30006
+ DOCUMENT_TEXT_PROPERTIES = new Set([
30007
+ "body",
30008
+ "text",
30009
+ "description",
30010
+ "content",
30011
+ "markdown",
30012
+ "prose",
30013
+ "summary",
30014
+ "details"
30015
+ ]);
29971
30016
  });
29972
30017
 
29973
30018
  // ../cli/dist/operations/semantic-advisor/analysis-receipt.js
@@ -52715,7 +52760,7 @@ var init_check_executor = __esm(() => {
52715
52760
  });
52716
52761
 
52717
52762
  // ../cli/dist/public/operations/specs/check-rules.generated.js
52718
- var SELECTABLE_RULE_NAMES, SELECTABLE_RULES_ALLOWED = "must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged";
52763
+ var SELECTABLE_RULE_NAMES, SELECTABLE_RULES_ALLOWED = "must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, related-requirement-unmodeled, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged";
52719
52764
  var init_check_rules_generated = __esm(() => {
52720
52765
  SELECTABLE_RULE_NAMES = [
52721
52766
  "must-priority-coverage",
@@ -52744,6 +52789,7 @@ var init_check_rules_generated = __esm(() => {
52744
52789
  "predicate-verifiability",
52745
52790
  "strict-readiness",
52746
52791
  "semantic-completeness",
52792
+ "related-requirement-unmodeled",
52747
52793
  "proof-contract-symbols",
52748
52794
  "entity-id-style",
52749
52795
  "domain-redundancy",
@@ -55289,7 +55335,7 @@ function buildRelationshipPlan(factId, requirementId, claimText, existingLogicCl
55289
55335
  claimText: claimText.trim(),
55290
55336
  logicClaims
55291
55337
  } : {},
55292
- instructions: "Apply the predicate fact first, update the requirement with the returned merged logicClaims manifest, then attach this relationship without overwriting other requirement metadata."
55338
+ instructions: `Apply the predicate fact ${factId} first, update the requirement with the returned merged logicClaims manifest, then attach this relationship without overwriting other requirement metadata. The relationship target is the planned fact id ${factId}, never a candidates[].id (SUGGEST-...).`
55293
55339
  };
55294
55340
  }
55295
55341
  function buildGapApplyPlan(text, args) {
@@ -55505,6 +55551,37 @@ function withMarginRejection(candidates) {
55505
55551
  };
55506
55552
  });
55507
55553
  }
55554
+ function unquoted(value) {
55555
+ return typeof value === "string" ? value.replace(/^['"]|['"]$/g, "") : "";
55556
+ }
55557
+ async function existingClaimGrounding(prolog, requirementId, claimKey, warnings) {
55558
+ if (prolog === null || !requirementId)
55559
+ return [];
55560
+ const found = [];
55561
+ try {
55562
+ for (const type of GROUNDING_RELATIONSHIP_TYPES) {
55563
+ const targets = await prolog.query(`findall(To, kb_relationship(${type}, '${escapeAtom(requirementId)}', To), Targets)`);
55564
+ if (!targets.success)
55565
+ continue;
55566
+ for (const target of parsePrologList(targets.bindings.Targets ?? "[]")) {
55567
+ 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 = '')))`);
55568
+ if (!fact.success)
55569
+ continue;
55570
+ if (unquoted(fact.bindings.ClaimKey) !== claimKey)
55571
+ continue;
55572
+ found.push({
55573
+ relationship: { type, from: requirementId, to: target },
55574
+ factId: target,
55575
+ factKind: unquoted(fact.bindings.FactKind) || null,
55576
+ claimKey
55577
+ });
55578
+ }
55579
+ }
55580
+ } catch (error) {
55581
+ 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.`);
55582
+ }
55583
+ return found;
55584
+ }
55508
55585
  async function handleKbSuggestPredicates(prolog, args, context) {
55509
55586
  const text = normalizeText2(args.text);
55510
55587
  const subject = inferSubject(text, args.subjectHint);
@@ -55569,12 +55646,22 @@ async function handleKbSuggestPredicates(prolog, args, context) {
55569
55646
  warnings.push("No predicate candidate met minScore. If this is recurring domain language, create a fact_kind=predicate_schema fact; otherwise keep the generated review:ontology-gap observation. Do not invent unsupported predicate names without a predicate_schema.");
55570
55647
  }
55571
55648
  const unavailableSchema = Boolean(args.schemaId && selectedSchemas.length === 0);
55572
- const recommendedAction = !recommendedCandidate ? unavailableSchema ? "resolve_schema_reference" : "record_ontology_gap" : recommendedCandidate.binding_status === "complete" ? "apply_requires_predicate" : "provide_argument_bindings";
55573
- const applyPlan = recommendedCandidate && recommendedCandidate.binding_status === "complete" ? buildPredicateApplyPlan(recommendedCandidate, args) : !recommendedCandidate && !unavailableSchema ? buildGapApplyPlan(text, args) : [];
55574
- const relationshipPlan = recommendedCandidate && recommendedCandidate.binding_status === "complete" ? buildRelationshipPlan(String(applyPlan[0]?.id ?? ""), args.requirementId, text, args.existingLogicClaims) : null;
55575
- const recommendedPredicateSchema = !recommendedCandidate && !unavailableSchema ? buildPredicateSchemaDraft(text, subject) : null;
55576
- const textSummary = recommendedCandidate && recommendedCandidate.binding_status === "complete" ? `Suggested ${candidates.length} predicate candidate(s). Top applicable match: ${recommendedCandidate.predicate_name}. Apply structured predicate facts before falling back to prose.` : recommendedCandidate ? `Matched ${recommendedCandidate.predicate_name}, but exact reviewed values are still required for: ${recommendedCandidate.unbound_arguments.join(", ")}. No apply plan was generated.` : unavailableSchema ? `Requested predicate schema ${args.schemaId} is unavailable or semantically inapplicable. No apply plan was generated.` : "No predicate candidate passed the semantic applicability gate; record an ontology gap and review the generated schema draft instead of silently writing prose.";
55577
55649
  const claimKey = semanticClaimKey(text);
55650
+ const existingGrounding = await existingClaimGrounding(prolog, args.requirementId, claimKey, warnings);
55651
+ const alreadyGrounded = existingGrounding.length > 0;
55652
+ const completeCandidate = recommendedCandidate && recommendedCandidate.binding_status === "complete" ? recommendedCandidate : undefined;
55653
+ const recommendedAction = alreadyGrounded ? "already_grounded" : !recommendedCandidate ? unavailableSchema ? "resolve_schema_reference" : "record_ontology_gap" : completeCandidate ? "apply_requires_predicate" : "provide_argument_bindings";
55654
+ const predicatePlan = completeCandidate ? buildPredicateApplyPlan(completeCandidate, args) : [];
55655
+ const applyPlan = alreadyGrounded ? [] : completeCandidate ? predicatePlan : !recommendedCandidate && !unavailableSchema ? buildGapApplyPlan(text, args) : [];
55656
+ const plannedFactId = typeof predicatePlan[0]?.id === "string" ? predicatePlan[0].id : null;
55657
+ const relationshipPlan = completeCandidate && !alreadyGrounded ? buildRelationshipPlan(plannedFactId ?? "", args.requirementId, text, args.existingLogicClaims) : null;
55658
+ const replacementPlan = alreadyGrounded && plannedFactId !== null && args.requirementId ? buildGroundingReplacementPlan(args.requirementId, plannedFactId, predicatePlan, existingGrounding) : null;
55659
+ const relationshipTarget = relationshipPlan !== null || replacementPlan !== null ? plannedFactId : null;
55660
+ if (alreadyGrounded) {
55661
+ warnings.push(`${args.requirementId} already grounds this claim (${claimKey}) through ${existingGrounding.map((row) => `${row.relationship.type} -> ${row.factId}`).join(", ")}. A modeled claim takes exactly one logical grounding relationship, so adding requires_predicate beside it fails the proposition-complete rule. Keep the existing grounding, or follow replacementPlan to swap it for the predicate.`);
55662
+ }
55663
+ const recommendedPredicateSchema = !recommendedCandidate && !unavailableSchema && !alreadyGrounded ? buildPredicateSchemaDraft(text, subject) : null;
55664
+ const textSummary = alreadyGrounded ? `${args.requirementId} already grounds this claim through ${existingGrounding.map((row) => `${row.relationship.type} -> ${row.factId}`).join(", ")}; no write plan was generated.${replacementPlan ? ` To use ${completeCandidate?.predicate_name} instead, follow replacementPlan and link requires_predicate to ${plannedFactId}.` : ""}` : completeCandidate ? `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).` : recommendedCandidate ? `Matched ${recommendedCandidate.predicate_name}, but exact reviewed values are still required for: ${recommendedCandidate.unbound_arguments.join(", ")}. No apply plan was generated.` : unavailableSchema ? `Requested predicate schema ${args.schemaId} is unavailable or semantically inapplicable. No apply plan was generated.` : "No predicate candidate passed the semantic applicability gate; record an ontology gap and review the generated schema draft instead of silently writing prose.";
55578
55665
  const logicClaims = Array.from(new Set([...args.existingLogicClaims ?? [], claimKey]));
55579
55666
  return {
55580
55667
  content: [{ type: "text", text: textSummary }],
@@ -55590,16 +55677,51 @@ async function handleKbSuggestPredicates(prolog, args, context) {
55590
55677
  recommendedPredicateSchema,
55591
55678
  applyPlan,
55592
55679
  relationshipPlan,
55680
+ relationshipTarget,
55681
+ existingGrounding,
55682
+ replacementPlan,
55593
55683
  warnings
55594
55684
  },
55595
55685
  applyPlan
55596
55686
  };
55597
55687
  }
55688
+ function buildGroundingReplacementPlan(requirementId, factId, predicatePlan, existing) {
55689
+ return {
55690
+ relationshipTarget: factId,
55691
+ steps: [
55692
+ {
55693
+ operation: "kb_upsert",
55694
+ input: predicatePlan[0],
55695
+ reason: "Write the predicate fact that will ground the claim."
55696
+ },
55697
+ {
55698
+ operation: "kb_delete",
55699
+ input: {
55700
+ relationships: existing.map((row) => row.relationship)
55701
+ },
55702
+ reason: "Retract the existing grounding link so the claim keeps exactly one logical grounding relationship."
55703
+ },
55704
+ {
55705
+ operation: "kb_upsert",
55706
+ input: {
55707
+ type: "req",
55708
+ id: requirementId,
55709
+ relationships: [
55710
+ { type: "requires_predicate", from: requirementId, to: factId }
55711
+ ]
55712
+ },
55713
+ reason: `Link requires_predicate to ${factId}; upsert the requirement with its stored title and status and this relationship only.`
55714
+ }
55715
+ ],
55716
+ 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.`
55717
+ };
55718
+ }
55598
55719
  async function executeSuggestPredicates(args, context) {
55599
55720
  return handleKbSuggestPredicates(context.prolog ?? null, args, context);
55600
55721
  }
55601
- var DEFAULT_MAX_CANDIDATES = 5, DEFAULT_MIN_SCORE2 = 0.35, NON_ASSERTIVE_PROPOSITION_ROLES;
55722
+ var DEFAULT_MAX_CANDIDATES = 5, DEFAULT_MIN_SCORE2 = 0.35, NON_ASSERTIVE_PROPOSITION_ROLES, GROUNDING_RELATIONSHIP_TYPES;
55602
55723
  var init_suggest_predicates = __esm(() => {
55724
+ init_serialization();
55603
55725
  init_analyze_prose();
55604
55726
  init_clauses();
55605
55727
  init_plugin_orchestration();
@@ -55615,6 +55737,11 @@ var init_suggest_predicates = __esm(() => {
55615
55737
  "example",
55616
55738
  "subjective"
55617
55739
  ]);
55740
+ GROUNDING_RELATIONSHIP_TYPES = [
55741
+ "requires_property",
55742
+ "requires_predicate",
55743
+ "requires_rule"
55744
+ ];
55618
55745
  });
55619
55746
 
55620
55747
  // ../cli/dist/public/operations/specs/modeling.js
@@ -62458,7 +62585,7 @@ async function saveAtomicMutation(prolog, goals, operation) {
62458
62585
  }
62459
62586
 
62460
62587
  // ../cli/dist/operations/mutation/workspace-mutation-lock.js
62461
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync28, rmdirSync, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
62588
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync28, renameSync as renameSync6, rmdirSync, unlinkSync as unlinkSync6, writeFileSync as writeFileSync8 } from "node:fs";
62462
62589
  import * as path52 from "node:path";
62463
62590
  function lockPath(workspaceRoot, lockFileRelative) {
62464
62591
  return path52.join(workspaceRoot, lockFileRelative);
@@ -62569,9 +62696,40 @@ function observeExistingLock(fileSystem, target, ownerPath, isProcessAlive) {
62569
62696
  }
62570
62697
  return {
62571
62698
  kind: "recovery",
62572
- description: `holder pid ${record.pid} is not live`
62699
+ description: `holder pid ${record.pid} is not live`,
62700
+ ...source === "lock owner metadata" ? { deadHolder: record } : {}
62573
62701
  };
62574
62702
  }
62703
+ function reclaimDeadLock(fileSystem, target, holder, token) {
62704
+ const rename = fileSystem.renameSync;
62705
+ if (rename === undefined)
62706
+ return "changed";
62707
+ const tombstone = `${target}.reclaimed-${token}`;
62708
+ try {
62709
+ rename(target, tombstone);
62710
+ } catch (error) {
62711
+ const code = errorCode(error);
62712
+ if (code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY")
62713
+ return "changed";
62714
+ throw new WorkspaceMutationLockError(`failed to reclaim workspace mutation lock at ${target} from dead pid ${holder.pid}: ${errorDetail(error)}`);
62715
+ }
62716
+ const tombstoneOwner = path52.join(tombstone, OWNER_FILE);
62717
+ let moved = null;
62718
+ try {
62719
+ moved = readLockRecord(fileSystem.readFileSync(tombstoneOwner));
62720
+ } catch {
62721
+ moved = null;
62722
+ }
62723
+ if (moved === null || moved.token !== holder.token) {
62724
+ try {
62725
+ rename(tombstone, target);
62726
+ } catch {}
62727
+ 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);
62728
+ }
62729
+ removeLockOwner(fileSystem, tombstone, tombstoneOwner, true);
62730
+ removeLockDirectory(fileSystem, tombstone);
62731
+ return "reclaimed";
62732
+ }
62575
62733
  function defaultIsProcessAlive(pid) {
62576
62734
  try {
62577
62735
  process.kill(pid, 0);
@@ -62625,6 +62783,7 @@ async function acquireWorkspaceMutationLock(workspaceRoot, options2 = {}) {
62625
62783
  throw new WorkspaceMutationLockError(`failed to create workspace mutation lock parent at ${parentPath} after ${parentResult.attempts} attempt(s): ${errorDetail(parentResult.error)}`);
62626
62784
  }
62627
62785
  let acquired = false;
62786
+ const reclaimed = [];
62628
62787
  let lastBlocker = "unknown";
62629
62788
  let lastObservationMissing = false;
62630
62789
  let missingOwnerRereads = 0;
@@ -62650,6 +62809,21 @@ async function acquireWorkspaceMutationLock(workspaceRoot, options2 = {}) {
62650
62809
  }
62651
62810
  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);
62652
62811
  }
62812
+ if (observation.kind === "recovery" && observation.deadHolder !== undefined && fileSystem.renameSync !== undefined && options2.reclaimDeadHolder?.(observation.deadHolder) === true) {
62813
+ const holder = observation.deadHolder;
62814
+ if (reclaimDeadLock(fileSystem, target, holder, token) === "reclaimed") {
62815
+ reclaimed.push({
62816
+ pid: holder.pid,
62817
+ token: holder.token,
62818
+ acquiredAt: holder.acquiredAt,
62819
+ reclaimedAt: now(),
62820
+ reclaimedByPid: process.pid
62821
+ });
62822
+ } else {
62823
+ await sleep(RETRY_INTERVAL_MS);
62824
+ }
62825
+ continue;
62826
+ }
62653
62827
  if (observation.kind === "recovery") {
62654
62828
  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);
62655
62829
  }
@@ -62698,6 +62872,7 @@ async function acquireWorkspaceMutationLock(workspaceRoot, options2 = {}) {
62698
62872
  }
62699
62873
  let releaseState = "owned";
62700
62874
  return {
62875
+ ...reclaimed.length > 0 ? { reclaimed } : {},
62701
62876
  release: () => {
62702
62877
  if (releaseState === "released")
62703
62878
  return;
@@ -62735,7 +62910,8 @@ var init_workspace_mutation_lock = __esm(() => {
62735
62910
  writeFileSync8(target, data, options2);
62736
62911
  },
62737
62912
  unlinkSync: (target) => unlinkSync6(target),
62738
- rmdirSync: (target) => rmdirSync(target)
62913
+ rmdirSync: (target) => rmdirSync(target),
62914
+ renameSync: (from, to) => renameSync6(from, to)
62739
62915
  };
62740
62916
  WorkspaceMutationLockError = class WorkspaceMutationLockError extends Error {
62741
62917
  name = "WorkspaceMutationLockError";
@@ -64090,7 +64266,7 @@ var init_symbols_coordinator2 = __esm(() => {
64090
64266
 
64091
64267
  // ../cli/dist/operations/mutation/symbol-refresh.js
64092
64268
  import { createHash as createHash29, randomUUID as randomUUID5 } from "node:crypto";
64093
- import { existsSync as existsSync29, readFileSync as readFileSync33, renameSync as renameSync6, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
64269
+ import { existsSync as existsSync29, readFileSync as readFileSync33, renameSync as renameSync7, unlinkSync as unlinkSync8, writeFileSync as writeFileSync11 } from "node:fs";
64094
64270
  import * as path58 from "node:path";
64095
64271
  function record2(value) {
64096
64272
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -64129,7 +64305,7 @@ function replaceArtifactAtomically(targetPath, content) {
64129
64305
  const temporary = `${targetPath}.kibi-tmp-${process.pid}-${randomUUID5()}`;
64130
64306
  try {
64131
64307
  writeFileSync11(temporary, content, "utf8");
64132
- renameSync6(temporary, targetPath);
64308
+ renameSync7(temporary, targetPath);
64133
64309
  } catch (error) {
64134
64310
  try {
64135
64311
  unlinkSync8(temporary);
@@ -65520,7 +65696,7 @@ function formatSyncSummary(summary) {
65520
65696
  }
65521
65697
 
65522
65698
  // ../cli/dist/commands/sync/manifest.js
65523
- import { existsSync as existsSync31, readFileSync as readFileSync35, renameSync as renameSync7, unlinkSync as unlinkSync10, writeFileSync as writeFileSync13 } from "node:fs";
65699
+ import { existsSync as existsSync31, readFileSync as readFileSync35, renameSync as renameSync8, unlinkSync as unlinkSync10, writeFileSync as writeFileSync13 } from "node:fs";
65524
65700
  import * as path60 from "node:path";
65525
65701
  function resolveDeps2(overrides) {
65526
65702
  return {
@@ -65529,7 +65705,7 @@ function resolveDeps2(overrides) {
65529
65705
  existsSync: existsSync31,
65530
65706
  parseYAML: load,
65531
65707
  readFileSync: readFileSync35,
65532
- renameSync: renameSync7,
65708
+ renameSync: renameSync8,
65533
65709
  unlinkSync: unlinkSync10,
65534
65710
  writeFileSync: writeFileSync13,
65535
65711
  writeCoordinateArtifact,
@@ -66427,7 +66603,7 @@ var init_persistence = __esm(() => {
66427
66603
  });
66428
66604
 
66429
66605
  // ../cli/dist/commands/sync/staging.js
66430
- import { existsSync as existsSync33, mkdirSync as mkdirSync11, renameSync as renameSync8, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "node:fs";
66606
+ import { existsSync as existsSync33, mkdirSync as mkdirSync11, renameSync as renameSync9, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "node:fs";
66431
66607
  import { copyFileSync as copyFileSync2 } from "node:fs";
66432
66608
  import * as path63 from "node:path";
66433
66609
  function resolveDeps3(overrides) {
@@ -66447,7 +66623,7 @@ function resolveDeps3(overrides) {
66447
66623
  },
66448
66624
  mkdirSync: mkdirSync11,
66449
66625
  moduleDir: import.meta.dirname,
66450
- renameSync: renameSync8,
66626
+ renameSync: renameSync9,
66451
66627
  rmSync: rmSync4,
66452
66628
  writeFileSync: writeFileSync14,
66453
66629
  ...overrides
@@ -66635,7 +66811,7 @@ var init_staging = __esm(() => {
66635
66811
  // ../cli/dist/commands/sync.js
66636
66812
  import { execSync as execSync4 } from "node:child_process";
66637
66813
  import { createHash as createHash31 } from "node:crypto";
66638
- import { existsSync as existsSync34, mkdirSync as mkdirSync12, readFileSync as readFileSync37, readdirSync as readdirSync9, renameSync as renameSync9, writeFileSync as writeFileSync15 } from "node:fs";
66814
+ import { existsSync as existsSync34, mkdirSync as mkdirSync12, readFileSync as readFileSync37, readdirSync as readdirSync9, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "node:fs";
66639
66815
  import * as path64 from "node:path";
66640
66816
  function relationshipKey2(relationship) {
66641
66817
  return `${relationship.type}\x00${relationship.from}\x00${relationship.to}`;
@@ -67449,11 +67625,11 @@ async function syncCommand(options2 = {}, runtime = {}) {
67449
67625
  throw new SyncError(`Recovery backup path already exists: ${recoveryBackupPath}`);
67450
67626
  }
67451
67627
  mkdirSync12(path64.dirname(recoveryBackupPath), { recursive: true });
67452
- renameSync9(livePath, recoveryBackupPath);
67628
+ renameSync10(livePath, recoveryBackupPath);
67453
67629
  try {
67454
- renameSync9(stagingPath, livePath);
67630
+ renameSync10(stagingPath, livePath);
67455
67631
  } catch (error) {
67456
- renameSync9(recoveryBackupPath, livePath);
67632
+ renameSync10(recoveryBackupPath, livePath);
67457
67633
  throw error;
67458
67634
  }
67459
67635
  } else if (rebuild && journaledLive) {
@@ -68686,7 +68862,7 @@ ${KIBI_BIN_RESOLVER}
68686
68862
 
68687
68863
  // ../cli/dist/commands/legacy-storage-migration.js
68688
68864
  import { createHash as createHash34 } from "node:crypto";
68689
- import { existsSync as existsSync37, mkdirSync as mkdirSync15, readFileSync as readFileSync40, readdirSync as readdirSync11, renameSync as renameSync11, rmSync as rmSync5, statSync as statSync12, writeFileSync as writeFileSync18 } from "node:fs";
68865
+ import { existsSync as existsSync37, mkdirSync as mkdirSync15, readFileSync as readFileSync40, readdirSync as readdirSync11, renameSync as renameSync12, rmSync as rmSync5, statSync as statSync12, writeFileSync as writeFileSync18 } from "node:fs";
68690
68866
  import * as path70 from "node:path";
68691
68867
  function stripGlobToRoot(pattern) {
68692
68868
  const normalized = pattern.replaceAll("\\", "/").replace(/\/+$/, "");
@@ -68801,7 +68977,7 @@ function applyLegacyStorageMigration(cwd, plan) {
68801
68977
  continue;
68802
68978
  }
68803
68979
  mkdirSync15(path70.dirname(destinationAbsolute), { recursive: true });
68804
- renameSync11(sourceAbsolute, destinationAbsolute);
68980
+ renameSync12(sourceAbsolute, destinationAbsolute);
68805
68981
  movedFiles.push(move.to);
68806
68982
  }
68807
68983
  const existing = readKbManifest(cwd);
@@ -72941,7 +73117,7 @@ var init_cli_json_command = __esm(() => {
72941
73117
  });
72942
73118
 
72943
73119
  // ../cli/dist/commands/migrate.js
72944
- import { existsSync as existsSync40, mkdirSync as mkdirSync16, readFileSync as readFileSync44, renameSync as renameSync12, writeFileSync as writeFileSync19 } from "node:fs";
73120
+ import { existsSync as existsSync40, mkdirSync as mkdirSync16, readFileSync as readFileSync44, renameSync as renameSync13, writeFileSync as writeFileSync19 } from "node:fs";
72945
73121
  import * as path76 from "node:path";
72946
73122
  function migrationStepsFor(fromVersion) {
72947
73123
  const start = Math.max(0, fromVersion ?? 0);
@@ -72982,7 +73158,7 @@ function writeTextAtomically(filePath, content) {
72982
73158
  mkdirSync16(path76.dirname(filePath), { recursive: true });
72983
73159
  const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
72984
73160
  writeFileSync19(tempPath, content, "utf8");
72985
- renameSync12(tempPath, filePath);
73161
+ renameSync13(tempPath, filePath);
72986
73162
  }
72987
73163
  function formatSchemaVersion(rawSchemaVersion, normalized) {
72988
73164
  if (normalized === null) {
@@ -74929,7 +75105,7 @@ async function executeSourceRecovery(args, context, onCommitted = () => {
74929
75105
  }
74930
75106
  async function executeBootstrapPlan(args, context, recovery = false, remainingActions, priorResults = [], onCommitted = () => {
74931
75107
  return;
74932
- }) {
75108
+ }, carry = {}) {
74933
75109
  const actions = recovery ? [...remainingActions ?? args.plan.actions] : validateBootstrapPlanShape(args);
74934
75110
  preflightBootstrapActions(actions, context);
74935
75111
  const prolog = context.prolog ?? await context.ensureProlog?.();
@@ -74990,10 +75166,31 @@ async function executeBootstrapPlan(args, context, recovery = false, remainingAc
74990
75166
  if (!context.fs)
74991
75167
  return;
74992
75168
  await context.fs.mkdir(path78.dirname(journalPath));
74993
- await context.fs.writeFile(journalPath, `${JSON.stringify({ version: 2, kind: "bootstrap", plan: args.plan, state, checkpoint: currentCheckpoint, ...activeActionId ? { activeActionId } : {}, results }, null, 2)}
75169
+ await context.fs.writeFile(journalPath, `${JSON.stringify({
75170
+ version: 2,
75171
+ kind: "bootstrap",
75172
+ plan: args.plan,
75173
+ state,
75174
+ checkpoint: currentCheckpoint,
75175
+ ...activeActionId ? { activeActionId } : {},
75176
+ results,
75177
+ ...carry.lockReclaims?.length ? { lockReclaims: carry.lockReclaims } : {},
75178
+ ...carry.interruptedActions?.length ? { interruptedActions: carry.interruptedActions } : {}
75179
+ }, null, 2)}
74994
75180
  `);
74995
75181
  };
74996
75182
  await writeJournal("applying", undefined, initialCheckpoint);
75183
+ const totalActions = priorResults.length + actions.length;
75184
+ const reportProgress = (message) => {
75185
+ try {
75186
+ context.onProgress?.({
75187
+ progress: results.filter((row) => row.outcome === "applied").length,
75188
+ total: totalActions,
75189
+ message
75190
+ });
75191
+ } catch {}
75192
+ };
75193
+ reportProgress(`Bootstrap plan ${args.plan.planHash.slice(0, 12)}: applying ${actions.length} of ${totalActions} action(s).`);
74997
75194
  try {
74998
75195
  for (const action of actions) {
74999
75196
  try {
@@ -75044,6 +75241,7 @@ async function executeBootstrapPlan(args, context, recovery = false, remainingAc
75044
75241
  stepsApplied: results.filter((row) => row.outcome === "applied").length,
75045
75242
  sourceHashesChecked: Object.keys(args.plan.expected.sourceHashes).length,
75046
75243
  notes: [
75244
+ ...carry.notes ?? [],
75047
75245
  "Bootstrap stopped after an authoritative action committed with derived repair effects."
75048
75246
  ]
75049
75247
  },
@@ -75070,6 +75268,7 @@ async function executeBootstrapPlan(args, context, recovery = false, remainingAc
75070
75268
  lastCheckpoint = await checkpoint();
75071
75269
  activeActionId = undefined;
75072
75270
  await writeJournal("applying", undefined, lastCheckpoint);
75271
+ reportProgress(`Applied ${action.id}.`);
75073
75272
  } catch (error) {
75074
75273
  const detail = error instanceof Error ? error.message : String(error);
75075
75274
  const deterministic = isDeterministicBootstrapFailure(error);
@@ -75102,6 +75301,7 @@ async function executeBootstrapPlan(args, context, recovery = false, remainingAc
75102
75301
  stepsApplied: results.filter((row) => row.outcome === "applied").length,
75103
75302
  sourceHashesChecked: Object.keys(args.plan.expected.sourceHashes).length,
75104
75303
  notes: [
75304
+ ...carry.notes ?? [],
75105
75305
  deterministic ? "Bootstrap stopped at a deterministic action failure; the journal is terminal. Re-plan from the current state." : "Bootstrap application stopped at a repairable action failure."
75106
75306
  ]
75107
75307
  },
@@ -75149,6 +75349,7 @@ async function executeBootstrapPlan(args, context, recovery = false, remainingAc
75149
75349
  stepsApplied: results.filter((row) => row.outcome === "applied").length,
75150
75350
  sourceHashesChecked: Object.keys(args.plan.expected.sourceHashes).length,
75151
75351
  notes: [
75352
+ ...carry.notes ?? [],
75152
75353
  "Bootstrap application stopped before its full action graph completed."
75153
75354
  ]
75154
75355
  },
@@ -75194,6 +75395,7 @@ async function executeBootstrapPlan(args, context, recovery = false, remainingAc
75194
75395
  stepsApplied: results.filter((row) => row.outcome === "applied").length,
75195
75396
  sourceHashesChecked: Object.keys(args.plan.expected.sourceHashes).length,
75196
75397
  notes: [
75398
+ ...carry.notes ?? [],
75197
75399
  "Bootstrap actions applied sequentially through the shared plan executor."
75198
75400
  ]
75199
75401
  }
@@ -75214,6 +75416,16 @@ async function executeApplyPlanUnlocked(args, context, onCommitted = () => {
75214
75416
  }
75215
75417
  return withRecoveryReport(result, recovered);
75216
75418
  }
75419
+ function interruptedBootstrapAction(journal, actionIds) {
75420
+ const active = journal.activeActionId;
75421
+ if (journal.state !== "applying" || typeof active !== "string")
75422
+ return;
75423
+ if (!actionIds.has(active))
75424
+ return;
75425
+ if ((journal.results ?? []).some((row) => row.actionId === active))
75426
+ return;
75427
+ return active;
75428
+ }
75217
75429
  async function dispatchApplyPlan(args, context, onCommitted, recovered) {
75218
75430
  if ("recoveryJournalId" in args) {
75219
75431
  if (isPlanApplyJournalId(args.recoveryJournalId))
@@ -75248,9 +75460,30 @@ async function dispatchApplyPlan(args, context, onCommitted, recovered) {
75248
75460
  }) : recoveryStatus?.snapshotId ?? "unavailable",
75249
75461
  workspaceSnapshot: recoveryWorkspace.available ? recoveryWorkspace.snapshot.hash : "unavailable"
75250
75462
  };
75251
- if (liveCheckpoint.branch !== journal.checkpoint.branch || liveCheckpoint.kbSnapshotId !== journal.checkpoint.kbSnapshotId || liveCheckpoint.workspaceSnapshot !== journal.checkpoint.workspaceSnapshot)
75252
- throw new Error("Bootstrap recovery refused: repository state changed since the last action checkpoint");
75253
75463
  const actionIds = new Set(ordered.map((action) => action.id));
75464
+ const drifted = liveCheckpoint.branch !== journal.checkpoint.branch || liveCheckpoint.kbSnapshotId !== journal.checkpoint.kbSnapshotId || liveCheckpoint.workspaceSnapshot !== journal.checkpoint.workspaceSnapshot;
75465
+ const interruptedActionId = drifted ? interruptedBootstrapAction(journal, actionIds) : undefined;
75466
+ if (drifted && (interruptedActionId === undefined || liveCheckpoint.branch !== journal.checkpoint.branch))
75467
+ throw new Error("Bootstrap recovery refused: repository state changed since the last action checkpoint");
75468
+ const recordsOf = (value) => Array.isArray(value) ? value.filter((row) => row !== null && typeof row === "object" && !Array.isArray(row)) : [];
75469
+ const interruptedActions = [
75470
+ ...recordsOf(journal.interruptedActions),
75471
+ ...interruptedActionId === undefined ? [] : [
75472
+ {
75473
+ actionId: interruptedActionId,
75474
+ recoveredAt: context.clock().toISOString(),
75475
+ checkpoint: journal.checkpoint,
75476
+ liveCheckpoint
75477
+ }
75478
+ ]
75479
+ ];
75480
+ const carry = {
75481
+ lockReclaims: recordsOf(journal.lockReclaims),
75482
+ interruptedActions,
75483
+ notes: interruptedActionId === undefined ? [] : [
75484
+ `Action ${interruptedActionId} was interrupted before its checkpoint; the state drift since the last checkpoint is attributed to it and the action was re-applied idempotently.`
75485
+ ]
75486
+ };
75254
75487
  const applied = new Set;
75255
75488
  for (const row of journal.results ?? []) {
75256
75489
  if (typeof row.actionId !== "string" || !actionIds.has(row.actionId))
@@ -75266,7 +75499,7 @@ async function dispatchApplyPlan(args, context, onCommitted, recovered) {
75266
75499
  outcome: row.outcome,
75267
75500
  detail: row.detail
75268
75501
  }
75269
- ] : [] : []), onCommitted);
75502
+ ] : [] : []), onCommitted, carry);
75270
75503
  }
75271
75504
  return executeSourceRecovery(args, context, onCommitted);
75272
75505
  }
@@ -75903,12 +76136,55 @@ function withRecoveryReport(result, recovered) {
75903
76136
  }
75904
76137
  return { content, structuredContent: result.structuredContent };
75905
76138
  }
76139
+ async function applyingBootstrapJournal(args, context) {
76140
+ if (!context.fs || !("recoveryJournalId" in args))
76141
+ return;
76142
+ if (!/^bootstrap-[a-f0-9]{16}$/.test(args.recoveryJournalId))
76143
+ return;
76144
+ const journalPath = path78.join(context.workspaceRoot, ".kb", "recovery", `${args.recoveryJournalId}.json`);
76145
+ try {
76146
+ const journal = JSON.parse(await context.fs.readFile(journalPath));
76147
+ return journal.kind === "bootstrap" && journal.state === "applying" ? journalPath : undefined;
76148
+ } catch {
76149
+ return;
76150
+ }
76151
+ }
76152
+ async function recordBootstrapLockReclaims(context, journalPath, reclaimed) {
76153
+ if (!context.fs)
76154
+ return;
76155
+ const journal = JSON.parse(await context.fs.readFile(journalPath));
76156
+ const earlier = Array.isArray(journal.lockReclaims) ? journal.lockReclaims : [];
76157
+ await context.fs.writeFile(journalPath, `${JSON.stringify({
76158
+ ...journal,
76159
+ lockReclaims: [
76160
+ ...earlier,
76161
+ ...reclaimed.map((row) => ({
76162
+ holderPid: row.pid,
76163
+ holderAcquiredAt: new Date(row.acquiredAt).toISOString(),
76164
+ reclaimedAt: new Date(row.reclaimedAt).toISOString(),
76165
+ reclaimedByPid: row.reclaimedByPid
76166
+ }))
76167
+ ]
76168
+ }, null, 2)}
76169
+ `);
76170
+ }
75906
76171
  async function executeApplyPlan(args, context) {
75907
76172
  if (isBootstrapApplyArgs(args))
75908
76173
  preflightBootstrapActions(validateBootstrapPlanShape(args), context);
75909
76174
  if (!context.fs || context.sourceMutationLockHeld === true)
75910
76175
  return executeApplyPlanUnlocked(args, context);
75911
- const lock = await acquireWorkspaceMutationLock(context.workspaceRoot);
76176
+ const applyingJournal = await applyingBootstrapJournal(args, context);
76177
+ const lock = await acquireWorkspaceMutationLock(context.workspaceRoot, applyingJournal === undefined ? {} : {
76178
+ reclaimDeadHolder: () => true
76179
+ });
76180
+ if (applyingJournal !== undefined && lock.reclaimed !== undefined) {
76181
+ try {
76182
+ await recordBootstrapLockReclaims(context, applyingJournal, lock.reclaimed);
76183
+ } catch (error) {
76184
+ releaseWorkspaceMutationLock(lock, { error });
76185
+ throw error;
76186
+ }
76187
+ }
75912
76188
  let operationFailure;
75913
76189
  let committed = false;
75914
76190
  try {
@@ -76380,7 +76656,12 @@ var init_planning = __esm(() => {
76380
76656
  recoveryJournalId: {
76381
76657
  type: "string",
76382
76658
  pattern: "^[A-Za-z0-9._-]+$",
76383
- description: "Typed recovery journal identifier returned by a committed_with_repairs result or a PLAN_APPLY_RECOVERY_REQUIRED error. Recovery replays only the immutable journal and never the original plan request; a compile-plan journal (plan-apply-*) is completed or rolled back deterministically and reports which."
76659
+ description: "Typed recovery journal identifier returned by a committed_with_repairs result or a PLAN_APPLY_RECOVERY_REQUIRED error. Recovery replays only the immutable journal and never the original plan request; a compile-plan journal (plan-apply-*) is completed or rolled back deterministically and reports which. A bootstrap journal left `applying` by a process that died mid-action is resumed without edits: the interrupted action is re-applied and a source lock held by the dead process is reclaimed and recorded in the journal."
76660
+ },
76661
+ async: {
76662
+ type: "boolean",
76663
+ default: false,
76664
+ description: "MCP only. When true, start the apply as a background job and return a kibi.job.v1 receipt immediately instead of holding the request open; poll kb_job_status with the returned jobId for the final result. Use for large bootstrap plans (hundreds of actions) that can outlast a client's request timeout. The MCP server registers kb_job_status only when KIBI_MCP_OPTIONAL_TOOLS includes it; otherwise async applies synchronously. A synchronous apply sends notifications/progress after each bootstrap action when the request carries a progressToken. The CLI ignores this field."
76384
76665
  }
76385
76666
  }
76386
76667
  },
@@ -2,7 +2,7 @@
2
2
  id: kibi-bootstrap
3
3
  name: kibi-bootstrap
4
4
  description: "Use for any Kibi bootstrap or onboarding task: seeding a new or thin knowledge base; reviewing a bootstrap or onboarding plan, preview, or evidence; judging approval readiness or coverage; diagnosing a blocked, partial, or failed bootstrap or its repair; applying an approved bootstrap plan; or asking the human which knowledge sources hold intent. Covers kb_status first, source interview, cited intent claims, read-only kb_plan_bootstrap preview, explicit approval, exact-plan kb_apply_plan, and repair-safe close-out."
5
- version: 3.4.0
5
+ version: 3.5.0
6
6
  kibiCompatibility: ">=1.0.0"
7
7
  tags:
8
8
  - kibi
@@ -38,13 +38,13 @@ Seed branch-local Kibi knowledge from cited product intent, without creating a p
38
38
  5. **Narrow with filters, not caps.** Read the `tldr` and the `Suppressed candidates by reason` diagnostic before individual rows. If the output is too large or mostly tooling or metadata, re-preview with `entityTypes` or declared `sourceOfTruthPaths`. Generic Markdown is off by default once you declare intent claims; set `includeGenericMarkdown: true` only for repository docs the human named. Keep confirmed intent claims: they are never capped and never use `maxCandidates`, which is the separate budget for candidates Kibi discovers (symbols, tests, repository docs). Raise it only when discovered candidates you need are `over_limit`. Read the per-source `Knowledge source …` diagnostics; if a source has claims not planned, report them per source and restate them before approval, or carry them to step 11 with their citation.
39
39
  6. **Request approval.** Show the complete `structuredContent.plan`, the full hash (never abbreviated), which candidates cite which sources, what was omitted, the open questions and declared conflicts it records for the human to decide, and any claim that contradicts code. If you find an undeclared contradiction now, add it to `conflicts` and preview again rather than resolving it. With no answer, stop as "awaiting approval." If declined, replan and ask again with the new hash.
40
40
  7. **Pre-apply check.** The plan you send must equal the returned plan: every top-level field, including `suppressedCandidates`, `diagnostics`, `candidates`, `actions`, and `expected`, with every array the same length and order. Never rebuild it from preview fields. If the host cannot pass the exact object, stop and say so.
41
- 8. **Apply once** with `kb_apply_plan`, the exact plan, and `approvedPlanHash`. The operation owns dependency ordering, source-first writes, sequential mutation, and recovery journaling.
41
+ 8. **Apply once** with `kb_apply_plan`, the exact plan, and `approvedPlanHash`. The operation owns dependency ordering, source-first writes, sequential mutation, and recovery journaling. A plan with hundreds of actions can run for minutes: pass a `progressToken` (Kibi sends progress after each action) with a client that resets its timeout on progress, or pass `async: true` and poll `kb_job_status` when the host has it. If the apply is cut off, call `kb_apply_plan` with only the `recoveryJournalId` from `kb_status` or the error; it resumes an action interrupted mid-write and reclaims a lock left by a dead process. Never edit or delete `.kb/recovery` files; if recovery is refused, stop and report it.
42
42
  9. **Read back** with `kb_query` and `kb_search` to confirm writes and citations.
43
43
  10. **Close out** with `kb_check`, then `kb_status`.
44
44
  11. **Deepen.** Bootstrap is complete; the KB is now half-way, holding cited `req` entries and review facts but no scenarios, tests or predicates. Load `kibi-usage` and continue in the normal Kibi workflow, one area at a time, telling the human what you author:
45
45
  - **Unplanned claims.** For every claim suppressed as `invalid_write` and every `sourceOnlySignals` entry ("Author requirement …"), restate the claim's statement and citation (`sourceId:reference`), model it with `kb_model` (`mode: "requirement"`, or `mode: "predicates"` for domain claims), and write the result with `kb_upsert` (`dryRun: true` first).
46
- - **Scenarios.** Write scenarios from the cited ticket's acceptance criteria, one scenario per criterion, not bulk drafts from the requirement title; ask the human when the source states none. Give each a plain title, `expects`, and `assumes` links to the `property_value` facts whose values the scenario relies on, then link the requirement with `specified_by` (upsert the `req` with its stored `title`, a `status` and that relationship only). Example: criterion "Given an annotation in draft state, when the user deletes it, the draft is cleared" becomes `{"type":"scenario","id":"SCEN-annotation-delete-clears-draft","properties":{"title":"Deleting a draft annotation clears the draft","status":"active","expects":"success"},"relationships":[{"type":"assumes","from":"SCEN-annotation-delete-clears-draft","to":"FACT-annotation-state-draft"}]}`, where the assumed fact is a `property_value` (`annotation.state` `eq` `draft`). Link existing tests that exercise it; never invent test evidence.
47
- - **Predicates.** Run `kb_model` with `mode: "predicates"` on each persisted requirement and apply a returned plan with `requires_predicate`, or record the `review:ontology-gap` observation it returns.
46
+ - **Scenarios.** Write scenarios from the cited ticket's acceptance criteria, one scenario per criterion, not bulk drafts from the requirement title; ask the human when the source states none. Give each a plain title, `expects`, the scenario prose in `document.body` (never in `properties`), and `assumes` links to the `property_value` facts whose values the scenario relies on, then link the requirement with `specified_by` (upsert the `req` with its stored `title`, a `status` and that relationship only). Example: criterion "Given an annotation in draft state, when the user deletes it, the draft is cleared" becomes `{"type":"scenario","id":"SCEN-annotation-delete-clears-draft","properties":{"title":"Deleting a draft annotation clears the draft","status":"active","expects":"success"},"document":{"body":"Given an annotation in draft state, when the user deletes it, then the draft is cleared and nothing is published.\n\nThe acceptance criterion comes from the cited ticket; reviewers asked for it so a deleted draft never reappears after reload.\n"},"relationships":[{"type":"assumes","from":"SCEN-annotation-delete-clears-draft","to":"FACT-annotation-state-draft"}]}`, where the assumed fact is a `property_value` (`annotation.state` `eq` `draft`). Link existing tests that exercise it; never invent test evidence.
47
+ - **Predicates.** Run `kb_model` with `mode: "predicates"` and `requirementId` on each persisted requirement and apply a returned plan with `requires_predicate` to its `relationshipTarget` (the planned `FACT-PRED-…` id, never a `SUGGEST-…` candidate id), or record the `review:ontology-gap` observation it returns, with its prose in `document.body`. `already_grounded` means the bootstrap already grounded that claim; leave it unless the human wants the `replacementPlan` swap. Count what `kb_query` shows afterwards, not the calls you made.
48
48
  - **Unrecorded ambiguity.** Record any conflict or open question you found but did not declare in step 3 as a cited `fact_kind: observation` tagged `review:conflict` or `review:open-question`.
49
49
  Finish with `kb_check` and `kb_status`.
50
50
  12. **Report** calls made, result statuses, applied scope, what step 11 authored, gaps, and follow-ups. Classify as complete, partial, awaiting approval, or blocked.
@@ -2,7 +2,7 @@
2
2
  id: kibi-usage
3
3
  name: Kibi Usage
4
4
  description: Use Kibi's source-first, exact-Git, migration-aware, proof-aware operations safely across MCP or the trusted local CLI, including partial completion repair.
5
- version: 2.4.0
5
+ version: 2.5.0
6
6
  kibiCompatibility: ">=1.0.0"
7
7
  tags:
8
8
  - kibi
@@ -263,10 +263,15 @@ prose. Bind them from the claim's own words and call again with the same
263
263
  ```
264
264
 
265
265
  When the retry returns `apply_requires_predicate`, write its `applyPlan` fact,
266
- then the `relationshipPlan` `requires_predicate` row. Record an ontology gap
266
+ then the `relationshipPlan` `requires_predicate` row; its target is
267
+ `relationshipTarget` (the planned `FACT-PRED-…` id), never a candidate's
268
+ `SUGGEST-…` id. `already_grounded` means the requirement already grounds the
269
+ claim (for example a bootstrap `requires_property` fact): a second grounding
270
+ link would fail, so keep it or follow `replacementPlan`. Record an ontology gap
267
271
  only when no returned schema fits the claim, not because bindings were missing.
268
- A review note is an observation that quotes its claim without `claim_key`:
269
- `{"type":"fact","id":"FACT-review-<area>-<behavior>","properties":{"title":"Review: ...","status":"active","fact_kind":"observation","text_ref":"<sourceId>:<reference>","tags":["review:invalid-write"],"claim_text":"<the claim>"}}`.
272
+ A review note is an observation that quotes its claim without `claim_key`, with
273
+ its prose in `document.body`:
274
+ `{"type":"fact","id":"FACT-review-<area>-<behavior>","properties":{"title":"Review: ...","status":"active","fact_kind":"observation","text_ref":"<sourceId>:<reference>","tags":["review:invalid-write"],"claim_text":"<the claim>"},"document":{"body":"<why it needs review, who raised it, the source>"}}`.
270
275
 
271
276
  To add `specified_by` (or another relationship) to an existing requirement,
272
277
  upsert it with its stored `title`, a `status` and the relationship only; Kibi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-runtime",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Narrow first-party runtime contracts and adapters for Kibi operations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "gray-matter": "^4.0.3",
21
- "kibi-core": "^0.16.0",
21
+ "kibi-core": "^0.17.0",
22
22
  "kibi-plugin-builtin": "^0.4.2",
23
23
  "kibi-swipl": "^1.0.1"
24
24
  },