toolroll 0.9.34 → 0.9.37

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 (195) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -0
  3. package/dist/assignment-adapters.d.ts +12 -48
  4. package/dist/assignment-adapters.js +33 -43
  5. package/dist/assignment.d.ts +1 -0
  6. package/dist/assignment.js +2 -2
  7. package/dist/browser-workspace.d.ts +3 -3
  8. package/dist/build-review.d.ts +1 -1
  9. package/dist/build-review.js +2 -1
  10. package/dist/builder.js +4 -7
  11. package/dist/chat-acceptance.d.ts +2 -2
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-decide.d.ts +14 -1
  16. package/dist/chat-decide.js +12 -6
  17. package/dist/chat-delivery-state.d.ts +12 -70
  18. package/dist/chat-delivery-state.js +17 -135
  19. package/dist/chat-delivery.js +58 -30
  20. package/dist/chat-flow.js +8 -6
  21. package/dist/chat-inbox.js +1 -1
  22. package/dist/coding-context.d.ts +2 -39
  23. package/dist/coding-context.js +2 -1
  24. package/dist/coding-handoff.d.ts +6 -25
  25. package/dist/coding-handoff.js +11 -5
  26. package/dist/coding-types.d.ts +2 -26
  27. package/dist/coding-workspace.d.ts +2 -0
  28. package/dist/coding-workspace.js +50 -14
  29. package/dist/contracts/acceptance-terms.d.ts +14 -0
  30. package/dist/contracts/acceptance-terms.js +16 -0
  31. package/dist/contracts/chat-actions.d.ts +328 -0
  32. package/dist/contracts/chat-actions.js +133 -0
  33. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  34. package/dist/contracts/chat-callback-rows.js +46 -0
  35. package/dist/contracts/chat-content.d.ts +276 -0
  36. package/dist/contracts/chat-content.js +127 -0
  37. package/dist/contracts/chat-tables.d.ts +6 -0
  38. package/dist/contracts/chat-tables.js +136 -0
  39. package/dist/contracts/coding-context.d.ts +97 -0
  40. package/dist/contracts/coding-context.js +60 -0
  41. package/dist/contracts/coding-handoff.d.ts +44 -0
  42. package/dist/contracts/coding-handoff.js +47 -0
  43. package/dist/contracts/coding-workspace.d.ts +165 -0
  44. package/dist/contracts/coding-workspace.js +71 -0
  45. package/dist/contracts/contract.d.ts +40 -7
  46. package/dist/contracts/contract.js +155 -19
  47. package/dist/contracts/decision.d.ts +45 -0
  48. package/dist/contracts/decision.js +45 -0
  49. package/dist/contracts/discord-callback.d.ts +45 -0
  50. package/dist/contracts/discord-callback.js +34 -0
  51. package/dist/contracts/flow-card.d.ts +50 -0
  52. package/dist/contracts/flow-card.js +46 -0
  53. package/dist/contracts/flow-propose.d.ts +325 -0
  54. package/dist/contracts/flow-propose.js +41 -0
  55. package/dist/contracts/flow-send.d.ts +119 -0
  56. package/dist/contracts/flow-send.js +112 -0
  57. package/dist/contracts/flow.d.ts +19405 -0
  58. package/dist/contracts/flow.js +364 -0
  59. package/dist/contracts/gateway-tools.d.ts +374 -0
  60. package/dist/contracts/gateway-tools.js +94 -0
  61. package/dist/contracts/handoff.d.ts +91 -0
  62. package/dist/contracts/handoff.js +82 -0
  63. package/dist/contracts/lead-context.d.ts +165 -0
  64. package/dist/contracts/lead-context.js +73 -0
  65. package/dist/contracts/lead-tools.d.ts +1905 -0
  66. package/dist/contracts/lead-tools.js +265 -0
  67. package/dist/contracts/memory-pass.d.ts +158 -0
  68. package/dist/contracts/memory-pass.js +102 -0
  69. package/dist/contracts/project-knowledge.d.ts +126 -0
  70. package/dist/contracts/project-knowledge.js +93 -0
  71. package/dist/contracts/project-memory.d.ts +117 -0
  72. package/dist/contracts/project-memory.js +51 -0
  73. package/dist/contracts/project-skills.d.ts +121 -0
  74. package/dist/contracts/project-skills.js +76 -0
  75. package/dist/contracts/proof.d.ts +136 -0
  76. package/dist/contracts/proof.js +110 -0
  77. package/dist/contracts/review-findings.d.ts +46 -0
  78. package/dist/contracts/review-findings.js +48 -0
  79. package/dist/contracts/route.d.ts +484 -0
  80. package/dist/contracts/route.js +128 -0
  81. package/dist/contracts/scope.d.ts +105 -0
  82. package/dist/contracts/scope.js +114 -0
  83. package/dist/contracts/scout-report.d.ts +234 -0
  84. package/dist/contracts/scout-report.js +141 -0
  85. package/dist/contracts/slack-callback.d.ts +43 -0
  86. package/dist/contracts/slack-callback.js +35 -0
  87. package/dist/contracts/sort-answer.d.ts +83 -0
  88. package/dist/contracts/sort-answer.js +77 -0
  89. package/dist/contracts/stage-output.d.ts +127 -0
  90. package/dist/contracts/stage-output.js +166 -0
  91. package/dist/contracts/task-sizing.d.ts +24 -0
  92. package/dist/contracts/task-sizing.js +28 -0
  93. package/dist/contracts/teammate-turn.d.ts +43 -0
  94. package/dist/contracts/teammate-turn.js +51 -0
  95. package/dist/contracts/teams-callback.d.ts +14 -0
  96. package/dist/contracts/teams-callback.js +14 -0
  97. package/dist/contracts/telegram-callback.d.ts +120 -0
  98. package/dist/contracts/telegram-callback.js +97 -0
  99. package/dist/contracts/verification-receipt.d.ts +148 -0
  100. package/dist/contracts/verification-receipt.js +87 -0
  101. package/dist/decision.d.ts +11 -20
  102. package/dist/decision.js +86 -167
  103. package/dist/demo.js +28 -11
  104. package/dist/discord-chat.js +31 -20
  105. package/dist/evidence.d.ts +3 -40
  106. package/dist/evidence.js +5 -1
  107. package/dist/flow-engine.js +14 -17
  108. package/dist/flow-gallery.d.ts +3 -2
  109. package/dist/flow-items.d.ts +4 -13
  110. package/dist/flow-items.js +1 -15
  111. package/dist/flow-send.d.ts +6 -37
  112. package/dist/flow-send.js +18 -14
  113. package/dist/flow-share.d.ts +10 -32
  114. package/dist/flow-share.js +72 -84
  115. package/dist/flow-sort.d.ts +7 -26
  116. package/dist/flow-sort.js +25 -20
  117. package/dist/flow-starters.d.ts +2 -1
  118. package/dist/flow-steps.js +3 -2
  119. package/dist/flow-triggers.d.ts +15 -72
  120. package/dist/flow-triggers.js +94 -83
  121. package/dist/flows.d.ts +37 -220
  122. package/dist/flows.js +356 -387
  123. package/dist/invoke.js +4 -4
  124. package/dist/lead-context.d.ts +4 -2
  125. package/dist/lead-context.js +24 -17
  126. package/dist/mate-tools.d.ts +4 -0
  127. package/dist/mate-tools.js +111 -308
  128. package/dist/mcp-connect.d.ts +27 -4
  129. package/dist/mcp-connect.js +92 -17
  130. package/dist/mcp.js +84 -216
  131. package/dist/memory-pass.d.ts +17 -42
  132. package/dist/memory-pass.js +54 -26
  133. package/dist/operate.js +31 -2
  134. package/dist/phase-routing.d.ts +19 -72
  135. package/dist/phase-routing.js +34 -164
  136. package/dist/plan.d.ts +5 -5
  137. package/dist/plan.js +5 -25
  138. package/dist/process-recovery-eligibility.d.ts +2 -2
  139. package/dist/process-recovery-eligibility.js +13 -8
  140. package/dist/project-knowledge.d.ts +16 -35
  141. package/dist/project-knowledge.js +42 -21
  142. package/dist/project-memory.d.ts +2 -28
  143. package/dist/project-memory.js +28 -13
  144. package/dist/project-skills.d.ts +16 -38
  145. package/dist/project-skills.js +41 -32
  146. package/dist/project-tools.d.ts +1 -0
  147. package/dist/project-tools.js +4 -4
  148. package/dist/proof.d.ts +15 -91
  149. package/dist/proof.js +90 -249
  150. package/dist/provider.d.ts +2 -0
  151. package/dist/provider.js +4 -16
  152. package/dist/result-evidence-readers.d.ts +42 -0
  153. package/dist/result-evidence-readers.js +91 -0
  154. package/dist/result-shots.d.ts +1 -1
  155. package/dist/result-shots.js +2 -1
  156. package/dist/review-switch.d.ts +3 -8
  157. package/dist/reviewer.d.ts +8 -12
  158. package/dist/reviewer.js +11 -35
  159. package/dist/scope.d.ts +21 -43
  160. package/dist/scope.js +42 -128
  161. package/dist/scout-report.d.ts +9 -194
  162. package/dist/scout-report.js +113 -264
  163. package/dist/scout.js +15 -11
  164. package/dist/serve.js +17 -101
  165. package/dist/slack-chat.js +26 -20
  166. package/dist/slack-state.js +2 -1
  167. package/dist/store.d.ts +9 -32
  168. package/dist/store.js +18 -15
  169. package/dist/structured-output.js +2 -1
  170. package/dist/subscription-chat.d.ts +4 -0
  171. package/dist/subscription-chat.js +4 -1
  172. package/dist/surface.js +8 -1
  173. package/dist/task-review-brief.d.ts +69 -0
  174. package/dist/task-review-brief.js +179 -0
  175. package/dist/task-sizing.d.ts +3 -27
  176. package/dist/task-sizing.js +11 -26
  177. package/dist/task-text.d.ts +0 -5
  178. package/dist/task-text.js +1 -3
  179. package/dist/teammate-question.js +7 -5
  180. package/dist/teammate-work.js +9 -5
  181. package/dist/teammates.d.ts +13 -64
  182. package/dist/teammates.js +32 -33
  183. package/dist/teams-chat.js +20 -11
  184. package/dist/telegram-flow.js +4 -3
  185. package/dist/telegram-mate.d.ts +4 -14
  186. package/dist/telegram-mate.js +8 -7
  187. package/dist/telegram-team.d.ts +2 -19
  188. package/dist/telegram.d.ts +1 -1
  189. package/dist/telegram.js +52 -42
  190. package/dist/text-limits.d.ts +212 -1
  191. package/dist/text-limits.js +214 -1
  192. package/dist/tools-ui.d.ts +2 -1
  193. package/dist/tools-ui.js +7 -4
  194. package/dist/verification-evidence.js +24 -12
  195. package/package.json +1 -1
package/dist/plan.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * the scope proposal is authority-bearing, so it gets the park discipline).
7
7
  */
8
8
  import { type AcceptanceCriterion } from "./scope.js";
9
- import type { ContractIssue } from "./contracts/contract.js";
9
+ import { type ContractIssue } from "./contracts/contract.js";
10
10
  export type PlanProblem = {
11
11
  reason: string;
12
12
  message: string;
@@ -71,11 +71,11 @@ export type PlanParseResult = {
71
71
  problems: PlanProblem[];
72
72
  };
73
73
  /**
74
- * A contract issue as a plan problem: the path-named line is the message, and the reason keeps the codes the rest of
75
- * the code relies on — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` —
76
- * so repair eligibility (document-only problems) and durable outcomes read the same as before the contract.
74
+ * A contract issue as a plan problem (`contractProblemOf`): the reason keeps the codes the rest of the code relies on
75
+ * — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` — so repair eligibility
76
+ * (document-only problems) and durable outcomes read the same as before the contract.
77
77
  */
78
- export declare function planProblemOf(issue: ContractIssue): PlanProblem;
78
+ export declare const planProblemOf: (issue: ContractIssue) => PlanProblem;
79
79
  /**
80
80
  * The planner's handoff, read against the plan contract (src/contracts/plan.ts): fail closed, every problem at once,
81
81
  * each one naming its path (`acceptance[0].evidence[0]: must be one of ...`). A plan saved before plans carried
package/dist/plan.js CHANGED
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
9
9
  import { hasForbiddenControls } from "./decision.js";
10
10
  import { parseAcceptanceCriteria } from "./scope.js";
11
11
  import { TEXT_LIMITS } from "./text-limits.js";
12
+ import { contractProblemOf } from "./contracts/contract.js";
12
13
  import { planPayloadBody, readPlanPayload } from "./contracts/plan.js";
13
14
  const EXECUTION_PLAN_SECTIONS = ["approach", "milestones", "dependencies", "risks", "proof"];
14
15
  const EXECUTION_PLAN_LIST_CAP = 12;
@@ -163,32 +164,11 @@ function prose(value, field, cap, required, problems) {
163
164
  return value;
164
165
  }
165
166
  /**
166
- * A contract issue as a plan problem: the path-named line is the message, and the reason keeps the codes the rest of
167
- * the code relies on — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` —
168
- * so repair eligibility (document-only problems) and durable outcomes read the same as before the contract.
167
+ * A contract issue as a plan problem (`contractProblemOf`): the reason keeps the codes the rest of the code relies on
168
+ * — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` — so repair eligibility
169
+ * (document-only problems) and durable outcomes read the same as before the contract.
169
170
  */
170
- export function planProblemOf(issue) {
171
- const at = issue.path;
172
- const reason = (() => {
173
- switch (issue.kind) {
174
- case "required":
175
- case "empty":
176
- case "too-few":
177
- return `missing-${at}`;
178
- case "too-long":
179
- return `${at}-too-long`;
180
- case "too-many":
181
- return `${at}-too-many`;
182
- case "unknown-key":
183
- return `${at}-unknown-key`;
184
- case "newer-version":
185
- return "newer-version";
186
- default:
187
- return `bad-${at}`;
188
- }
189
- })();
190
- return { reason, message: issue.line };
191
- }
171
+ export const planProblemOf = contractProblemOf;
192
172
  /** scope.ts's rubric problem as a path-named line: `acceptance[0].id: is over 40 bytes`. */
193
173
  function acceptanceLine(problem) {
194
174
  const named = /^(acceptance(?:\[\d+\])?(?:\.[A-Za-z]+)?) (.*)$/.exec(problem.message);
@@ -37,7 +37,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
37
37
  leaseId: string;
38
38
  role: "builder";
39
39
  outcome: "built" | "no-change";
40
- committed: boolean | null;
40
+ committed: boolean;
41
41
  startedAt: string;
42
42
  finishedAt: string;
43
43
  branch: string;
@@ -81,7 +81,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
81
81
  readonly state: import("./proof.js").CriterionMatrixState;
82
82
  readonly detail: readonly string[];
83
83
  readonly answered: readonly {
84
- readonly kind: import("./scope.js").EvidenceKind;
84
+ readonly kind: "changed-path" | "check" | "manual-review" | "screenshot";
85
85
  readonly ref: string;
86
86
  }[];
87
87
  readonly review: {
@@ -8,6 +8,8 @@
8
8
  import { createHash } from "node:crypto";
9
9
  import { normalizeBootId } from "./boot-identity.js";
10
10
  import { scopeApprovedForDispatch } from "./dispatch.js";
11
+ import { parseHandoffArtifact } from "./contracts/handoff.js";
12
+ import { readVerificationReceipt } from "./contracts/verification-receipt.js";
11
13
  import { readVerifiedArtifact } from "./evidence.js";
12
14
  import { ownedProcessCount, runOwnerTag } from "./exec.js";
13
15
  import { scopeAuthorityOf } from "./scope.js";
@@ -97,11 +99,11 @@ export function preparedCandidateObserverGapEligibility(store, input) {
97
99
  const gate = verificationEvidence(store, evidenceRoot, runId);
98
100
  if (!gate.ok || gate.bytes === null)
99
101
  return refuse("verification-evidence-unavailable");
100
- const receipt = JSON.parse(gate.bytes);
101
- if (receipt.version !== 1 || receipt.source !== undefined || receipt.reusedFrom !== undefined || receipt.executedHere !== undefined ||
102
- receipt.result?.configured !== true || receipt.result.ran !== true || receipt.result.exitCode !== 0 ||
103
- Object.keys(receipt.result).some(key => !["configured", "ran", "exitCode", "setupReplayed"].includes(key)) ||
104
- (receipt.result.setupReplayed !== undefined && receipt.result.setupReplayed !== true) ||
102
+ // A legacy log-header view is not a sealed receipt: the strict schema refuses its extra keys.
103
+ const view = JSON.parse(gate.bytes);
104
+ const receipt = readVerificationReceipt(view);
105
+ if (!receipt.ok || view.version !== 1 || receipt.value.reusedFrom !== undefined || receipt.value.executedHere !== undefined ||
106
+ receipt.value.result.configured !== true || receipt.value.result.ran !== true || receipt.value.result.exitCode !== 0 ||
105
107
  !instant(gateArtifact.createdAt) || Date.parse(gateArtifact.createdAt) < Date.parse(run.startedAt) ||
106
108
  Date.parse(gateArtifact.createdAt) > Date.parse(machine.decidedAt) ||
107
109
  Date.parse(gateArtifact.createdAt) > Date.parse(run.finishedAt))
@@ -115,12 +117,15 @@ export function preparedCandidateObserverGapEligibility(store, input) {
115
117
  const saved = readVerifiedArtifact(evidenceRoot, artifact);
116
118
  if (!saved.ok)
117
119
  return refuse("machine-prepared-handoff-unavailable");
118
- const handoff = JSON.parse(saved.content.toString("utf8"));
120
+ const read = parseHandoffArtifact(saved.content.toString("utf8"));
121
+ if (!read.ok)
122
+ return refuse("machine-prepared-handoff-mismatch");
123
+ const handoff = read.value;
119
124
  const conclusion = `Prepared candidate ${scope.candidate} was checked out by the machine; no agent ran. ${run.outcome === "no-change" ? "The branch already matched it." : "The sealed diff spans this task's base to that candidate."}`;
120
- if (handoff.schema !== 1 || handoff.runId !== runId || handoff.taskId !== ref.externalId || handoff.sessionId !== null ||
125
+ if (handoff.runId !== runId || handoff.taskId !== ref.externalId || handoff.sessionId !== null ||
121
126
  handoff.provider !== run.provider || handoff.branch !== run.branch || handoff.worktree !== run.worktree ||
122
127
  handoff.base !== run.baseRevision || handoff.head !== run.headRevision || handoff.outcome !== run.outcome ||
123
- handoff.committed !== run.committed || handoff.freshness?.currentAsOf !== run.headRevision ||
128
+ handoff.committed !== run.committed || handoff.freshness.currentAsOf !== run.headRevision ||
124
129
  handoff.conclusion !== conclusion || run.handoff !== conclusion ||
125
130
  !instant(artifact.createdAt) || Date.parse(artifact.createdAt) < Date.parse(run.startedAt) ||
126
131
  Date.parse(artifact.createdAt) > Date.parse(gateArtifact.createdAt))
@@ -1,26 +1,7 @@
1
- import { type DecisionLine } from './project-memory.js';
2
- import { type RepositoryContext } from './repository-context.js';
3
1
  import type { Store } from './store.js';
2
+ import { type Knowledge, type KnowledgeDraft, type KnowledgeReference, type KnowledgeSelection } from './contracts/project-knowledge.js';
4
3
  export declare const KNOWLEDGE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_knowledge (\n repo TEXT PRIMARY KEY, identity TEXT NOT NULL, revision INTEGER NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS knowledge_change (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL,\n actor TEXT NOT NULL, at TEXT NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL,\n UNIQUE(repo,identity,revision)\n);\nCREATE TABLE IF NOT EXISTS knowledge_snapshot (\n run INTEGER PRIMARY KEY REFERENCES run(id), repo TEXT NOT NULL, identity TEXT NOT NULL,\n payload TEXT NOT NULL, sha TEXT NOT NULL\n);\n-- A saved project identity carried forward (a restart renumbered the volume, or an approver applied it).\nCREATE TABLE IF NOT EXISTS project_identity_carry (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, from_identity TEXT NOT NULL, to_identity TEXT NOT NULL,\n actor TEXT NOT NULL, at TEXT NOT NULL, how TEXT NOT NULL CHECK(how IN ('automatic','approver')),\n UNIQUE(repo,from_identity,to_identity)\n);\nCREATE TRIGGER IF NOT EXISTS project_identity_carry_no_update BEFORE UPDATE ON project_identity_carry BEGIN SELECT RAISE(ABORT,'Project identity history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS project_identity_carry_no_delete BEFORE DELETE ON project_identity_carry BEGIN SELECT RAISE(ABORT,'Project identity history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_change_no_update BEFORE UPDATE ON knowledge_change BEGIN SELECT RAISE(ABORT,'Knowledge history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_change_no_delete BEFORE DELETE ON knowledge_change BEGIN SELECT RAISE(ABORT,'Knowledge history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_snapshot_no_update BEFORE UPDATE ON knowledge_snapshot BEGIN SELECT RAISE(ABORT,'Knowledge context is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS knowledge_snapshot_no_delete BEFORE DELETE ON knowledge_snapshot BEGIN SELECT RAISE(ABORT,'Knowledge context is immutable'); END;\n";
5
- export type KnowledgeReference = {
6
- id: string;
7
- title: string;
8
- content: string;
9
- path: string | null;
10
- sourceSha: string | null;
11
- sourceRevision: string | null;
12
- };
13
- export type Knowledge = {
14
- instructions: string;
15
- references: KnowledgeReference[];
16
- };
17
- export type KnowledgeDraft = {
18
- instructions?: string;
19
- title?: string;
20
- content?: string;
21
- path?: string;
22
- id?: string;
23
- };
4
+ export type { Knowledge, KnowledgeDraft, KnowledgeReference, KnowledgeSelection };
24
5
  export declare const STALE_KNOWLEDGE = "This project's saved knowledge is from before a change; review and apply it";
25
6
  export declare function knowledgeView(store: Store, repo: string, actor: string): {
26
7
  repo: string;
@@ -53,7 +34,20 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
53
34
  decisions?: never;
54
35
  } | {
55
36
  stale?: never;
56
- decision: import("./project-memory.js").Decision;
37
+ decision: {
38
+ repo: string;
39
+ revision: number;
40
+ claim: string;
41
+ why: string;
42
+ status: "active" | "retired" | "superseded";
43
+ supersedes: number | null;
44
+ decidedBy: string;
45
+ decidedAt: string;
46
+ sourceKind: "backward-pass" | "conversation" | "manual" | "result" | "task";
47
+ sourceRef: string | null;
48
+ recordedBy: string;
49
+ id: number;
50
+ };
57
51
  notice: string;
58
52
  title?: never;
59
53
  content?: never;
@@ -110,19 +104,6 @@ export declare function changeKnowledge(store: Store, args: {
110
104
  * Returns the new revision, or null when nothing was waiting.
111
105
  */
112
106
  export declare function applySavedKnowledge(store: Store, repo: string, actor: string, now?: Date): number | null;
113
- export type KnowledgeSelection = {
114
- version: 1;
115
- revision: number;
116
- instructions: string;
117
- references: KnowledgeReference[];
118
- omitted: {
119
- title: string;
120
- reason: string;
121
- }[];
122
- inheritedFrom: number | null;
123
- repository?: RepositoryContext;
124
- decisions?: DecisionLine[];
125
- };
126
107
  /** Read the same bounded, source-checked selection without inventing a worker run. */
127
108
  export declare function selectProjectKnowledge(store: Store, args: {
128
109
  repo: string;
@@ -5,6 +5,9 @@ import { currentActor } from './actor.js';
5
5
  import { decisionLines, getDecision, listDecisions } from './project-memory.js';
6
6
  import { scanForSecrets } from './evidence.js';
7
7
  import { repositoryContextRead } from './repository-context.js';
8
+ import { TEXT_LIMITS } from './text-limits.js';
9
+ import { contractError } from './contracts/contract.js';
10
+ import { KNOWLEDGE_COUNTS, KNOWLEDGE_SELECTION_VERSION, KNOWLEDGE_VERSION, knowledgeSchema, knowledgeSelectionSchema, readKnowledge, readKnowledgeSelection } from './contracts/project-knowledge.js';
8
11
  export const KNOWLEDGE_SCHEMA = `
9
12
  CREATE TABLE IF NOT EXISTS project_knowledge (
10
13
  repo TEXT PRIMARY KEY, identity TEXT NOT NULL, revision INTEGER NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL
@@ -49,10 +52,21 @@ export const STALE_KNOWLEDGE = "This project's saved knowledge is from before a
49
52
  const STALE_OMITTED = { title: 'Saved project knowledge', reason: 'Saved knowledge is from before a change; it was not applied' };
50
53
  /** Revisions under any identity this project accepts, as a bound JSON list. */
51
54
  const IN_ACCEPTED = 'identity IN (SELECT value FROM json_each(?))';
55
+ /** The stored bytes are checked against their digest first; only then are they parsed, and upgraded in memory. */
52
56
  function decode(payload, sha) {
53
57
  if (learningSha(payload) !== sha)
54
58
  throw Error('Project knowledge could not be verified.');
55
- return JSON.parse(payload);
59
+ const read = readKnowledge(JSON.parse(payload));
60
+ if (!read.ok)
61
+ throw Error('Project knowledge could not be verified.');
62
+ return read.value;
63
+ }
64
+ /** Knowledge (or a selection) as it is written: through its schema, so a refusal names the field. */
65
+ function encode(schema, value, what) {
66
+ const read = schema.safeParse(value, { reportInput: true });
67
+ if (!read.success)
68
+ throw Error(`${what} could not be saved: ${contractError(read.error).join('; ')}`);
69
+ return JSON.stringify(read.data);
56
70
  }
57
71
  /** Saved knowledge under another identity is stale: it is returned for review, never applied. */
58
72
  function current(store, repo, accepted) {
@@ -73,9 +87,9 @@ function document(repo, revision, path) {
73
87
  if (!match)
74
88
  throw Error('This reference must be a committed text file, not a folder or link.');
75
89
  const sha = match[1];
76
- if (Number(git(repo, ['cat-file', '-s', sha])) > 12000)
90
+ if (Number(git(repo, ['cat-file', '-s', sha])) > TEXT_LIMITS.knowledgeReferenceBytes)
77
91
  throw Error('Choose a shorter reference (up to 12 KB).');
78
- return { content: clean(git(repo, ['cat-file', 'blob', sha]), 12000), sha };
92
+ return { content: clean(git(repo, ['cat-file', 'blob', sha]), TEXT_LIMITS.knowledgeReferenceBytes), sha };
79
93
  }
80
94
  export function knowledgeView(store, repo, actor) {
81
95
  const { identity, accepted } = admission(store, repo, actor), { stale, ...value } = current(store, repo, accepted);
@@ -128,7 +142,7 @@ export function changeKnowledge(store, args, now = new Date()) {
128
142
  throw Error('Knowledge changed in another window. Review your draft below before saving again.');
129
143
  let knowledge = existing.knowledge;
130
144
  if (args.action === 'instructions')
131
- knowledge.instructions = clean(args.draft.instructions ?? '', 4000);
145
+ knowledge.instructions = clean(args.draft.instructions ?? '', TEXT_LIMITS.knowledgeInstructionsBytes);
132
146
  else if (args.action === 'restore') {
133
147
  const row = store.handle.prepare(`SELECT payload,sha FROM knowledge_change WHERE repo=? AND ${IN_ACCEPTED} AND revision=?`).get(args.repo, JSON.stringify([...accepted]), args.restore ?? -1);
134
148
  if (!row)
@@ -141,26 +155,26 @@ export function changeKnowledge(store, args, now = new Date()) {
141
155
  knowledge.references = knowledge.references.filter(r => r.id !== args.draft.id);
142
156
  }
143
157
  else if (args.action === 'save') {
144
- const title = clean(args.draft.title ?? '', 120);
158
+ const title = clean(args.draft.title ?? '', TEXT_LIMITS.knowledgeTitleBytes);
145
159
  if (!title)
146
160
  throw Error('Give this reference a short name.');
147
161
  const id = args.draft.id || learningSha(`${identity}:${existing.revision + 1}:${title}`).slice(0, 20);
148
162
  if (args.draft.id && !knowledge.references.some(r => r.id === id))
149
163
  throw Error('That reference is no longer available.');
150
- const path = clean(args.draft.path ?? '', 300) || null;
164
+ const path = clean(args.draft.path ?? '', TEXT_LIMITS.knowledgePathBytes) || null;
151
165
  const sourceRevision = path ? git(args.repo, ['rev-parse', 'HEAD']) : null;
152
166
  const source = path ? document(args.repo, sourceRevision, path) : null;
153
- const content = source?.content ?? clean(args.draft.content ?? '', 12000);
167
+ const content = source?.content ?? clean(args.draft.content ?? '', TEXT_LIMITS.knowledgeReferenceBytes);
154
168
  if (!content)
155
169
  throw Error('Add reference text or select a committed project document.');
156
170
  const ref = { id, title, content, path, sourceSha: source?.sha ?? null, sourceRevision };
157
171
  knowledge.references = [...knowledge.references.filter(r => r.id !== id), ref];
158
- if (knowledge.references.length > 12)
172
+ if (knowledge.references.length > KNOWLEDGE_COUNTS.references)
159
173
  throw Error('Keep up to 12 focused references. Remove one before adding another.');
160
174
  }
161
175
  else
162
176
  throw Error('Choose a supported knowledge action.');
163
- const payload = JSON.stringify(knowledge), sha = learningSha(payload), revision = existing.revision + 1;
177
+ const payload = encode(knowledgeSchema, { version: KNOWLEDGE_VERSION, ...knowledge }, 'Project knowledge'), sha = learningSha(payload), revision = existing.revision + 1;
164
178
  store.handle.prepare('INSERT INTO knowledge_change(repo,identity,revision,actor,at,payload,sha) VALUES (?,?,?,?,?,?,?)').run(args.repo, identity, revision, args.actor, now.toISOString(), payload, sha);
165
179
  store.handle.prepare('INSERT INTO project_knowledge VALUES (?,?,?,?,?) ON CONFLICT(repo) DO UPDATE SET identity=excluded.identity,revision=excluded.revision,payload=excluded.payload,sha=excluded.sha').run(args.repo, identity, revision, payload, sha);
166
180
  });
@@ -191,7 +205,7 @@ function select(store, repo, accepted, query, head) {
191
205
  const ranked = knowledge.references.map(ref => ({ ref, score: [...tokens(`${ref.title} ${ref.path ?? ''}`)].reduce((n, t) => n + (q.has(t) ? 3 : 0), 0) + [...tokens(ref.content)].reduce((n, t) => n + (q.has(t) ? 1 : 0), 0) })).sort((a, b) => b.score - a.score || a.ref.id.localeCompare(b.ref.id));
192
206
  const references = [];
193
207
  for (const { ref, score } of ranked) {
194
- let reason = score === 0 ? 'Not relevant to this task' : references.length >= 3 ? 'More relevant references selected' : '';
208
+ let reason = score === 0 ? 'Not relevant to this task' : references.length >= KNOWLEDGE_COUNTS.selected ? 'More relevant references selected' : '';
195
209
  if (ref.path) {
196
210
  try {
197
211
  if (document(repo, head, ref.path).sha !== ref.sourceSha)
@@ -201,7 +215,7 @@ function select(store, repo, accepted, query, head) {
201
215
  reason = 'Source unavailable; refresh this reference';
202
216
  }
203
217
  }
204
- if (!reason && Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references: [...references, ref] })) > 24000)
218
+ if (!reason && Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references: [...references, ref] })) > TEXT_LIMITS.knowledgeContextBytes)
205
219
  reason = 'Context size limit';
206
220
  if (reason)
207
221
  omitted.push({ title: ref.title, reason });
@@ -210,8 +224,8 @@ function select(store, repo, accepted, query, head) {
210
224
  }
211
225
  // Settled decisions ride the same frozen record, one line each; the why
212
226
  // loads on demand by id, so nothing is repeated between brief and store.
213
- const decisions = decisionLines(store, repo, query, { limit: 8, bytes: Math.max(0, Math.min(1500, 24000 - Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references })) - 200)) });
214
- return { version: 1, revision, instructions: knowledge.instructions, references, omitted, inheritedFrom: null, ...(decisions.length ? { decisions } : {}) };
227
+ const decisions = decisionLines(store, repo, query, { limit: KNOWLEDGE_COUNTS.decisions, bytes: Math.max(0, Math.min(TEXT_LIMITS.knowledgeDecisionsBytes, TEXT_LIMITS.knowledgeContextBytes - Buffer.byteLength(JSON.stringify({ instructions: knowledge.instructions, references })) - 200)) });
228
+ return { version: KNOWLEDGE_SELECTION_VERSION, revision, instructions: knowledge.instructions, references, omitted, inheritedFrom: null, ...(decisions.length ? { decisions } : {}) };
215
229
  }
216
230
  /** Read the same bounded, source-checked selection without inventing a worker run. */
217
231
  export function selectProjectKnowledge(store, args) {
@@ -227,10 +241,17 @@ export function readKnowledgeSnapshot(store, runId) {
227
241
  if (!row)
228
242
  return null;
229
243
  const run = store.getRun(runId), repo = run && store.refForId(run.taskRef)?.repo;
230
- const selection = JSON.parse(String(row['payload']));
244
+ const unverified = () => Error('The context saved for this run could not be verified.');
245
+ // The stored bytes are checked against their digest before they are parsed.
246
+ if (!repo || row['repo'] !== repo || learningSha(String(row['payload'])) !== row['sha'])
247
+ throw unverified();
248
+ const read = readKnowledgeSelection(JSON.parse(String(row['payload'])));
249
+ if (!read.ok)
250
+ throw unverified();
251
+ const selection = read.value;
231
252
  const empty = row['identity'] === 'unconfigured' && unconfigured(selection);
232
- if (!repo || row['repo'] !== repo || (!empty && !acceptedIdentities(store, repo).has(String(row['identity']))) || learningSha(String(row['payload'])) !== row['sha'])
233
- throw Error('The context saved for this run could not be verified.');
253
+ if (!empty && !acceptedIdentities(store, repo).has(String(row['identity'])))
254
+ throw unverified();
234
255
  return selection;
235
256
  }
236
257
  /** Freeze at provider admission. Reviewers see the builder's exact project context. */
@@ -259,18 +280,18 @@ export function knowledgeContext(store, runId, cacheRoot, validatedBuilderBase)
259
280
  const configured = !!store.handle.prepare('SELECT 1 FROM project_knowledge WHERE repo=?').get(repo);
260
281
  // A legacy source without a snapshot did not receive this feature's
261
282
  // context. Never give its reviewer newly configured project guidance.
262
- selection = inherited ? { ...inherited, inheritedFrom: parent.id } : !configured || parent ? { version: 1, revision: 0, instructions: '', references: [], omitted: [], inheritedFrom: parent?.id ?? null } : select(store, repo, acceptedIdentities(store, repo), `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}`, contextBase || git(repo, ['rev-parse', 'HEAD']));
283
+ selection = inherited ? { ...inherited, inheritedFrom: parent.id } : !configured || parent ? { version: KNOWLEDGE_SELECTION_VERSION, revision: 0, instructions: '', references: [], omitted: [], inheritedFrom: parent?.id ?? null } : select(store, repo, acceptedIdentities(store, repo), `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}`, contextBase || git(repo, ['rev-parse', 'HEAD']));
263
284
  // Optional source selection is captured once from this crew's actual
264
285
  // checkout. It is reused with the immutable run snapshot on resume.
265
286
  // Index/source failure stays context metadata, never an admission gate.
266
287
  if (!parent && run.worktree && contextBase) {
267
- const available = 24000 - Buffer.byteLength(JSON.stringify(selection)) - 30;
268
- if (available >= 3000)
269
- selection.repository = repositoryContextRead({ repo: run.worktree, project: repo, baseRevision: contextBase, audience: 'crew', maxBytes: Math.min(6000, available), ...(cacheRoot === undefined ? {} : { cacheRoot }), query: `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}` });
288
+ const available = TEXT_LIMITS.knowledgeContextBytes - Buffer.byteLength(JSON.stringify(selection)) - 30;
289
+ if (available >= TEXT_LIMITS.knowledgeRepositoryMinBytes)
290
+ selection.repository = repositoryContextRead({ repo: run.worktree, project: repo, baseRevision: contextBase, audience: 'crew', maxBytes: Math.min(TEXT_LIMITS.knowledgeRepositoryBytes, available), ...(cacheRoot === undefined ? {} : { cacheRoot }), query: `${store.getTask(ref.externalId)?.title ?? ''} ${scope?.goal ?? ''} ${(scope?.touches ?? []).join(' ')}` });
270
291
  else
271
292
  selection.omitted.push({ title: 'Repository context', reason: 'Context size limit' });
272
293
  }
273
- const payload = JSON.stringify(selection);
294
+ const payload = encode(knowledgeSelectionSchema, selection, 'Project context');
274
295
  store.handle.prepare('INSERT INTO knowledge_snapshot VALUES (?,?,?,?,?)').run(runId, repo, unconfigured(selection) ? 'unconfigured' : learningIdentity(repo), payload, learningSha(payload));
275
296
  }
276
297
  // JSON escapes ordinary newlines, but not Unicode line separators. Keep
@@ -1,28 +1,7 @@
1
1
  import type { Store } from './store.js';
2
+ import { type Decision, type DecisionDraft, type DecisionLine } from './contracts/project-memory.js';
3
+ export type { Decision, DecisionDraft, DecisionLine };
2
4
  export declare const MEMORY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS project_decision (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1,\n claim TEXT NOT NULL, why TEXT NOT NULL,\n status TEXT NOT NULL CHECK(status IN ('active','superseded','retired')),\n supersedes INTEGER REFERENCES project_decision(id),\n decided_by TEXT NOT NULL, decided_at TEXT NOT NULL,\n source_kind TEXT NOT NULL CHECK(source_kind IN ('conversation','task','result','manual','backward-pass')),\n source_ref TEXT, recorded_by TEXT NOT NULL, sha TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS project_decision_repo ON project_decision(repo, status, id);\nCREATE TABLE IF NOT EXISTS decision_change (\n id INTEGER PRIMARY KEY, decision INTEGER NOT NULL REFERENCES project_decision(id), revision INTEGER NOT NULL,\n actor TEXT NOT NULL, at TEXT NOT NULL, action TEXT NOT NULL, payload TEXT NOT NULL, sha TEXT NOT NULL,\n UNIQUE(decision, revision)\n);\nCREATE TRIGGER IF NOT EXISTS decision_change_no_update BEFORE UPDATE ON decision_change BEGIN SELECT RAISE(ABORT,'Decision history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS decision_change_no_delete BEFORE DELETE ON decision_change BEGIN SELECT RAISE(ABORT,'Decision history is immutable'); END;\nCREATE TRIGGER IF NOT EXISTS project_decision_no_delete BEFORE DELETE ON project_decision BEGIN SELECT RAISE(ABORT,'Decisions are retired, never deleted'); END;\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_search USING fts5(kind UNINDEXED, ref UNINDEXED, repo UNINDEXED, scope UNINDEXED, title, body, tokenize='unicode61');\n-- The backward pass (memory-pass.ts): sessions analysed once per surface\n-- version, gaps that accumulate corroboration across runs, and the\n-- proposals a person accepts or rejects. Rejections are remembered.\nCREATE TABLE IF NOT EXISTS memory_session (\n id TEXT PRIMARY KEY, repo TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('claude','codex','lead','crew')),\n source TEXT NOT NULL, seen_at TEXT NOT NULL, surface TEXT NOT NULL, trace_sha TEXT NOT NULL,\n analyzed_at TEXT, verdict TEXT, problem TEXT\n);\nCREATE INDEX IF NOT EXISTS memory_session_repo ON memory_session(repo, seen_at);\nCREATE TABLE IF NOT EXISTS memory_gap (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, key TEXT NOT NULL, mistake TEXT NOT NULL, proposed TEXT NOT NULL,\n domain TEXT NOT NULL CHECK(domain IN ('project','orchestration')), first_seen TEXT NOT NULL, last_seen TEXT NOT NULL,\n retired_at TEXT, retired_reason TEXT, UNIQUE(repo, key)\n);\nCREATE TABLE IF NOT EXISTS memory_sighting (\n gap INTEGER NOT NULL REFERENCES memory_gap(id), session TEXT NOT NULL REFERENCES memory_session(id),\n at TEXT NOT NULL, quote TEXT NOT NULL, PRIMARY KEY(gap, session)\n);\nCREATE TABLE IF NOT EXISTS memory_proposal (\n id INTEGER PRIMARY KEY, repo TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('instruction-add','instruction-remove','decision-add')),\n fingerprint TEXT NOT NULL, title TEXT NOT NULL, rationale TEXT NOT NULL, before_text TEXT, after_text TEXT NOT NULL,\n evidence TEXT NOT NULL, sessions INTEGER NOT NULL, status TEXT NOT NULL CHECK(status IN ('pending','accepted','rejected','stale')),\n created_at TEXT NOT NULL, decided_by TEXT, decided_at TEXT, surface TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS memory_proposal_pending ON memory_proposal(repo, status, fingerprint);\nCREATE TABLE IF NOT EXISTS memory_rejection (\n repo TEXT NOT NULL, fingerprint TEXT NOT NULL, rejected_at TEXT NOT NULL, rejected_by TEXT NOT NULL, sessions INTEGER NOT NULL, PRIMARY KEY(repo, fingerprint)\n);\n";
3
- export type Decision = {
4
- id: number;
5
- repo: string;
6
- revision: number;
7
- claim: string;
8
- why: string;
9
- status: 'active' | 'superseded' | 'retired';
10
- supersedes: number | null;
11
- decidedBy: string;
12
- decidedAt: string;
13
- sourceKind: 'conversation' | 'task' | 'result' | 'manual' | 'backward-pass';
14
- sourceRef: string | null;
15
- recordedBy: string;
16
- };
17
- export type DecisionDraft = {
18
- claim: string;
19
- why: string;
20
- decidedBy?: string;
21
- decidedAt?: string;
22
- sourceKind?: Decision['sourceKind'];
23
- sourceRef?: string | null;
24
- supersedes?: number | null;
25
- };
26
5
  export declare function listDecisions(store: Store, repo: string, actor: string, options?: {
27
6
  status?: Decision['status'] | 'all';
28
7
  limit?: number;
@@ -47,11 +26,6 @@ export declare function decisionHistory(store: Store, repo: string, actor: strin
47
26
  at: string;
48
27
  action: string;
49
28
  }[];
50
- export type DecisionLine = {
51
- id: number;
52
- claim: string;
53
- decidedAt: string;
54
- };
55
29
  /**
56
30
  * The decisions worth one line each in a brief: relevant to the query first,
57
31
  * then newest, within a byte budget. Bodies (the why) load on demand by id,
@@ -10,6 +10,9 @@
10
10
  import { createHash } from 'node:crypto';
11
11
  import { scanForSecrets } from './evidence.js';
12
12
  import { learningIdentity } from './project-learning.js';
13
+ import { TEXT_LIMITS } from './text-limits.js';
14
+ import { contractError } from './contracts/contract.js';
15
+ import { DECISION_CHANGE_VERSION, decisionChangeSchema, decisionRecordSchema, decisionSchema } from './contracts/project-memory.js';
13
16
  export const MEMORY_SCHEMA = `
14
17
  CREATE TABLE IF NOT EXISTS project_decision (
15
18
  id INTEGER PRIMARY KEY, repo TEXT NOT NULL, identity TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1,
@@ -75,18 +78,29 @@ function admission(store, repo, actor, write = false) {
75
78
  throw Error('Project memory is outside your access.');
76
79
  return learningIdentity(repo);
77
80
  }
81
+ /** A decision row through its schema; null when the row does not read as one (it is then unverifiable). */
78
82
  function decisionOf(row) {
79
- return { id: Number(row['id']), repo: String(row['repo']), revision: Number(row['revision']), claim: String(row['claim']), why: String(row['why']),
83
+ const read = decisionSchema.safeParse({ id: Number(row['id']), repo: String(row['repo']), revision: Number(row['revision']), claim: String(row['claim']), why: String(row['why']),
80
84
  status: row['status'], supersedes: row['supersedes'] === null ? null : Number(row['supersedes']),
81
85
  decidedBy: String(row['decided_by']), decidedAt: String(row['decided_at']), sourceKind: row['source_kind'],
82
- sourceRef: row['source_ref'] === null ? null : String(row['source_ref']), recordedBy: String(row['recorded_by']) };
86
+ sourceRef: row['source_ref'] === null ? null : String(row['source_ref']), recordedBy: String(row['recorded_by']) });
87
+ return read.success ? read.data : null;
83
88
  }
84
89
  const digestOf = (d) => sha(JSON.stringify([d.repo, d.revision, d.claim, d.why, d.status, d.supersedes, d.decidedBy, d.decidedAt, d.sourceKind, d.sourceRef, d.recordedBy]));
90
+ /** A record or history entry through its schema before it is written; a refusal names the field. */
91
+ function checked(schema, value) {
92
+ const read = schema.safeParse(value);
93
+ if (!read.success)
94
+ throw Error(`This decision could not be saved: ${contractError(read.error).join('; ')}`);
95
+ return read.data;
96
+ }
97
+ const history = (payload) => JSON.stringify(checked(decisionChangeSchema, { version: DECISION_CHANGE_VERSION, ...payload }));
85
98
  export function listDecisions(store, repo, actor, options = {}) {
86
99
  admission(store, repo, actor);
87
100
  const status = options.status ?? 'active', limit = Math.min(200, Math.max(1, options.limit ?? 50));
88
101
  return store.handle.prepare(`SELECT * FROM project_decision WHERE repo=? AND (?='all' OR status=?) ORDER BY id DESC LIMIT ?`).all(repo, status, status, limit)
89
- .map(row => decisionOf(row)).filter(d => digestOf(d) === String(store.handle.prepare('SELECT sha FROM project_decision WHERE id=?').get(d.id)['sha']));
102
+ .flatMap(row => { const d = decisionOf(row); return d === null ? [] : [d]; })
103
+ .filter(d => digestOf(d) === String(store.handle.prepare('SELECT sha FROM project_decision WHERE id=?').get(d.id)['sha']));
90
104
  }
91
105
  export function getDecision(store, repo, actor, id) {
92
106
  admission(store, repo, actor);
@@ -94,20 +108,20 @@ export function getDecision(store, repo, actor, id) {
94
108
  if (!row)
95
109
  return null;
96
110
  const d = decisionOf(row);
97
- if (digestOf(d) !== String(row['sha']))
111
+ if (d === null || digestOf(d) !== String(row['sha']))
98
112
  throw Error('This decision could not be verified.');
99
113
  return d;
100
114
  }
101
115
  /** Record one settled choice. A superseding decision retires the older one in the same transaction. */
102
116
  export function recordDecision(store, args, now = new Date()) {
103
117
  const identity = admission(store, args.repo, args.actor, true);
104
- const claim = clean(args.draft.claim, 240, 'The decision'), why = clean(args.draft.why, 2000, 'The reason');
105
- const decidedBy = args.draft.decidedBy === undefined ? args.actor : clean(args.draft.decidedBy, 80, 'Who decided');
118
+ const claim = clean(args.draft.claim, TEXT_LIMITS.decisionClaimBytes, 'The decision'), why = clean(args.draft.why, TEXT_LIMITS.decisionWhyBytes, 'The reason');
119
+ const decidedBy = args.draft.decidedBy === undefined ? args.actor : clean(args.draft.decidedBy, TEXT_LIMITS.decisionByBytes, 'Who decided');
106
120
  const decidedAt = args.draft.decidedAt ?? now.toISOString();
107
121
  if (!/^\d{4}-\d{2}-\d{2}T/.test(decidedAt))
108
122
  throw Error('When it was decided must be an ISO timestamp.');
109
123
  const sourceKind = args.draft.sourceKind ?? 'manual';
110
- const sourceRef = args.draft.sourceRef === undefined || args.draft.sourceRef === null ? null : clean(String(args.draft.sourceRef), 200, 'The source');
124
+ const sourceRef = args.draft.sourceRef === undefined || args.draft.sourceRef === null ? null : clean(String(args.draft.sourceRef), TEXT_LIMITS.decisionSourceBytes, 'The source');
111
125
  return store.transact(() => {
112
126
  let supersedes = null;
113
127
  if (args.draft.supersedes !== undefined && args.draft.supersedes !== null) {
@@ -116,11 +130,11 @@ export function recordDecision(store, args, now = new Date()) {
116
130
  throw Error('The decision being replaced is not active.');
117
131
  supersedes = older.id;
118
132
  }
119
- const record = { repo: args.repo, revision: 1, claim, why, status: 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, recordedBy: args.actor };
133
+ const record = checked(decisionRecordSchema, { repo: args.repo, revision: 1, claim, why, status: 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, recordedBy: args.actor });
120
134
  const inserted = store.handle.prepare(`INSERT INTO project_decision(repo,identity,revision,claim,why,status,supersedes,decided_by,decided_at,source_kind,source_ref,recorded_by,sha)
121
135
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(args.repo, identity, 1, claim, why, 'active', supersedes, decidedBy, decidedAt, sourceKind, sourceRef, args.actor, digestOf(record));
122
136
  const id = Number(inserted.lastInsertRowid);
123
- store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, 1, args.actor, now.toISOString(), 'record', JSON.stringify(record), digestOf(record));
137
+ store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, 1, args.actor, now.toISOString(), 'record', history(record), digestOf(record));
124
138
  if (supersedes !== null)
125
139
  changeStatus(store, args.repo, args.actor, supersedes, 'superseded', `Replaced by decision ${id}.`, now);
126
140
  reindexProjectMemory(store, args.repo);
@@ -133,15 +147,16 @@ function changeStatus(store, repo, actor, id, status, reason, now) {
133
147
  throw Error('That decision is unavailable.');
134
148
  if (existing.status !== 'active')
135
149
  throw Error('That decision is no longer active.');
136
- const next = { ...existing, revision: existing.revision + 1, status };
150
+ const { id: _id, ...was } = existing;
151
+ const next = { ...was, revision: existing.revision + 1, status };
137
152
  store.handle.prepare('UPDATE project_decision SET revision=?,status=?,sha=? WHERE id=?').run(next.revision, status, digestOf(next), id);
138
- store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, next.revision, actor, now.toISOString(), status, JSON.stringify({ reason }), digestOf(next));
153
+ store.handle.prepare('INSERT INTO decision_change(decision,revision,actor,at,action,payload,sha) VALUES (?,?,?,?,?,?,?)').run(id, next.revision, actor, now.toISOString(), status, history({ reason }), digestOf(next));
139
154
  return { id, ...next };
140
155
  }
141
156
  /** Retire a decision that no longer holds; history keeps it. */
142
157
  export function retireDecision(store, args, now = new Date()) {
143
158
  admission(store, args.repo, args.actor, true);
144
- const reason = clean(args.reason, 500, 'The reason');
159
+ const reason = clean(args.reason, TEXT_LIMITS.decisionRetireBytes, 'The reason');
145
160
  return store.transact(() => { const d = changeStatus(store, args.repo, args.actor, args.id, 'retired', reason, now); reindexProjectMemory(store, args.repo); return d; });
146
161
  }
147
162
  export function decisionHistory(store, repo, actor, id) {
@@ -166,7 +181,7 @@ export function decisionLines(store, repo, query, options = {}) {
166
181
  return { line: { id: Number(row['id']), claim, decidedAt: String(row['decided_at']) }, score };
167
182
  })
168
183
  .sort((a, b) => b.score - a.score || b.line.id - a.line.id);
169
- const limit = options.limit ?? 8, bytes = options.bytes ?? 1500, out = [];
184
+ const limit = options.limit ?? 8, bytes = options.bytes ?? TEXT_LIMITS.knowledgeDecisionsBytes, out = [];
170
185
  for (const { line } of ranked) {
171
186
  if (out.length >= limit)
172
187
  break;
@@ -1,48 +1,27 @@
1
1
  import type { Store } from "./store.js";
2
+ import { SKILL_LIMITS, type SavedSkill, type SkillFile, type SkillPackage, type SkillSelection, type SkillsSnapshot } from "./contracts/project-skills.js";
3
+ export { SKILL_LIMITS };
4
+ export type { SavedSkill, SkillFile, SkillPackage, SkillsSnapshot };
2
5
  export declare const SKILLS_SCHEMA: string;
3
- export type SkillFile = {
4
- path: string;
5
- base64: string;
6
- };
7
- export type SkillPackage = {
8
- name: string;
9
- description: string;
10
- source: string;
11
- requirements: string;
12
- warnings: string[];
13
- files: SkillFile[];
14
- };
15
- export type SavedSkill = SkillPackage & {
16
- sha: string;
17
- };
18
- type Choice = {
19
- sha: string;
20
- enabled: boolean;
21
- };
22
- type Selection = Record<string, Choice>;
23
- export type SkillsSnapshot = {
24
- version: 1;
25
- revision: number;
26
- identity: string;
27
- inheritedFrom: number | null;
28
- test: boolean;
29
- packages: SavedSkill[];
30
- };
31
- export declare const SKILL_LIMITS: {
32
- files: number;
33
- packageBytes: number;
34
- fileBytes: number;
35
- bodyBytes: number;
36
- enabled: number;
37
- selectionBytes: number;
38
- };
6
+ type Selection = SkillSelection;
39
7
  export declare function validateSkill(files: SkillFile[], source: string): SkillPackage;
40
8
  export declare function skillsView(store: Store, repo: string, actor: string): {
41
9
  revision: number;
42
10
  selection: Selection;
43
11
  repo: string;
44
12
  identity: string;
45
- library: SavedSkill[];
13
+ library: {
14
+ name: string;
15
+ description: string;
16
+ source: string;
17
+ requirements: string;
18
+ warnings: string[];
19
+ files: {
20
+ path: string;
21
+ base64: string;
22
+ }[];
23
+ sha: string;
24
+ }[];
46
25
  history: {
47
26
  revision: number;
48
27
  actor: string;
@@ -159,4 +138,3 @@ export declare function githubSkill(url: string, fetcher?: typeof fetch): Promis
159
138
  files: SkillFile[];
160
139
  source: string;
161
140
  }>;
162
- export {};