@coreyuan/vector-mind 1.0.53 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +99 -72
  2. package/admin-panel/README.md +77 -0
  3. package/admin-panel/dist/client/assets/index-Bzyjdhh2.css +1 -0
  4. package/admin-panel/dist/client/assets/index-Ph73ewzt.js +9 -0
  5. package/admin-panel/dist/client/index.html +14 -0
  6. package/admin-panel/package.json +25 -0
  7. package/admin-panel/server/index.mjs +1424 -0
  8. package/admin-panel/server/security.mjs +117 -0
  9. package/admin-panel/server/security.test.mjs +113 -0
  10. package/admin-panel/server/storage.mjs +49 -0
  11. package/admin-panel/server/storage.test.mjs +36 -0
  12. package/dist/builtin-conventions.js +1 -1
  13. package/dist/builtin-conventions.js.map +1 -1
  14. package/dist/builtin-instructions.d.ts +1 -1
  15. package/dist/builtin-instructions.js +1 -1
  16. package/dist/builtin-instructions.js.map +1 -1
  17. package/dist/checkpoint-snapshot.d.ts +56 -0
  18. package/dist/checkpoint-snapshot.js +411 -0
  19. package/dist/checkpoint-snapshot.js.map +1 -0
  20. package/dist/config.d.ts +4 -1
  21. package/dist/config.js +25 -1
  22. package/dist/config.js.map +1 -1
  23. package/dist/context-governance.d.ts +56 -0
  24. package/dist/context-governance.js +176 -0
  25. package/dist/context-governance.js.map +1 -0
  26. package/dist/database-runtime.d.ts +6 -1
  27. package/dist/database-runtime.js +555 -127
  28. package/dist/database-runtime.js.map +1 -1
  29. package/dist/development-warnings.d.ts +2 -1
  30. package/dist/development-warnings.js +92 -22
  31. package/dist/development-warnings.js.map +1 -1
  32. package/dist/file-indexing.d.ts +2 -1
  33. package/dist/file-indexing.js +127 -47
  34. package/dist/file-indexing.js.map +1 -1
  35. package/dist/grep.js +4 -0
  36. package/dist/grep.js.map +1 -1
  37. package/dist/index.js +8 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/large-file-split.d.ts +53 -8
  40. package/dist/large-file-split.js +294 -75
  41. package/dist/large-file-split.js.map +1 -1
  42. package/dist/memory-maintenance/compaction.d.ts +7 -0
  43. package/dist/memory-maintenance/compaction.js +133 -0
  44. package/dist/memory-maintenance/compaction.js.map +1 -0
  45. package/dist/memory-maintenance/context.d.ts +37 -0
  46. package/dist/memory-maintenance/context.js +52 -0
  47. package/dist/memory-maintenance/context.js.map +1 -0
  48. package/dist/memory-maintenance/index-prune.d.ts +22 -0
  49. package/dist/memory-maintenance/index-prune.js +243 -0
  50. package/dist/memory-maintenance/index-prune.js.map +1 -0
  51. package/dist/memory-maintenance/metrics.d.ts +5 -0
  52. package/dist/memory-maintenance/metrics.js +30 -0
  53. package/dist/memory-maintenance/metrics.js.map +1 -0
  54. package/dist/memory-maintenance/pending.d.ts +4 -0
  55. package/dist/memory-maintenance/pending.js +74 -0
  56. package/dist/memory-maintenance/pending.js.map +1 -0
  57. package/dist/memory-maintenance/purge.d.ts +7 -0
  58. package/dist/memory-maintenance/purge.js +115 -0
  59. package/dist/memory-maintenance/purge.js.map +1 -0
  60. package/dist/memory-maintenance/runner.d.ts +5 -0
  61. package/dist/memory-maintenance/runner.js +182 -0
  62. package/dist/memory-maintenance/runner.js.map +1 -0
  63. package/dist/memory-maintenance/sqlite.d.ts +10 -0
  64. package/dist/memory-maintenance/sqlite.js +67 -0
  65. package/dist/memory-maintenance/sqlite.js.map +1 -0
  66. package/dist/memory-maintenance/types.d.ts +87 -0
  67. package/dist/memory-maintenance/types.js +2 -0
  68. package/dist/memory-maintenance/types.js.map +1 -0
  69. package/dist/memory-maintenance.d.ts +5 -80
  70. package/dist/memory-maintenance.js +3 -536
  71. package/dist/memory-maintenance.js.map +1 -1
  72. package/dist/memory-mutations.d.ts +3 -3
  73. package/dist/memory-mutations.js +109 -43
  74. package/dist/memory-mutations.js.map +1 -1
  75. package/dist/memory-recall.d.ts +3 -1
  76. package/dist/memory-recall.js +33 -1
  77. package/dist/memory-recall.js.map +1 -1
  78. package/dist/operation-scope.js +42 -12
  79. package/dist/operation-scope.js.map +1 -1
  80. package/dist/path-containment.d.ts +5 -0
  81. package/dist/path-containment.js +64 -0
  82. package/dist/path-containment.js.map +1 -0
  83. package/dist/path-filters.d.ts +1 -1
  84. package/dist/path-filters.js +7 -6
  85. package/dist/path-filters.js.map +1 -1
  86. package/dist/path-rules.d.ts +1 -0
  87. package/dist/path-rules.js +23 -1
  88. package/dist/path-rules.js.map +1 -1
  89. package/dist/pending-changes.d.ts +5 -0
  90. package/dist/pending-changes.js +30 -16
  91. package/dist/pending-changes.js.map +1 -1
  92. package/dist/project-files.d.ts +1 -0
  93. package/dist/project-files.js +19 -10
  94. package/dist/project-files.js.map +1 -1
  95. package/dist/root.d.ts +4 -1
  96. package/dist/root.js +34 -4
  97. package/dist/root.js.map +1 -1
  98. package/dist/rtk-integrity.d.ts +5 -0
  99. package/dist/rtk-integrity.js +35 -0
  100. package/dist/rtk-integrity.js.map +1 -0
  101. package/dist/rtk-shim.js +22 -9
  102. package/dist/rtk-shim.js.map +1 -1
  103. package/dist/rtk-tools.d.ts +8 -0
  104. package/dist/rtk-tools.js +59 -42
  105. package/dist/rtk-tools.js.map +1 -1
  106. package/dist/server-instructions.js +8 -88
  107. package/dist/server-instructions.js.map +1 -1
  108. package/dist/tool-catalog.d.ts +2 -0
  109. package/dist/tool-catalog.js +77 -24
  110. package/dist/tool-catalog.js.map +1 -1
  111. package/dist/tool-handlers/context-recovery.js +79 -54
  112. package/dist/tool-handlers/context-recovery.js.map +1 -1
  113. package/dist/tool-handlers/large-files.js +615 -41
  114. package/dist/tool-handlers/large-files.js.map +1 -1
  115. package/dist/tool-handlers/memory-diagnostics.js +240 -45
  116. package/dist/tool-handlers/memory-diagnostics.js.map +1 -1
  117. package/dist/tool-handlers/memory.js +167 -22
  118. package/dist/tool-handlers/memory.js.map +1 -1
  119. package/dist/tool-handlers/operations.js +28 -8
  120. package/dist/tool-handlers/operations.js.map +1 -1
  121. package/dist/tool-handlers/requirement-status.d.ts +5 -0
  122. package/dist/tool-handlers/requirement-status.js +267 -0
  123. package/dist/tool-handlers/requirement-status.js.map +1 -0
  124. package/dist/tool-handlers/requirements.js +851 -115
  125. package/dist/tool-handlers/requirements.js.map +1 -1
  126. package/dist/tool-handlers.d.ts +4 -0
  127. package/dist/tool-handlers.js +439 -6
  128. package/dist/tool-handlers.js.map +1 -1
  129. package/dist/tool-output.d.ts +71 -2
  130. package/dist/tool-output.js +56 -13
  131. package/dist/tool-output.js.map +1 -1
  132. package/dist/tool-schemas.d.ts +243 -0
  133. package/dist/tool-schemas.js +112 -26
  134. package/dist/tool-schemas.js.map +1 -1
  135. package/dist/types.d.ts +10 -1
  136. package/docs/capability-matrix.md +23 -17
  137. package/package.json +21 -5
  138. package/skills/vector-mind-autopilot/SKILL.md +82 -23
  139. package/skills/vector-mind-autopilot/references/claude-project-instructions.md +14 -11
  140. package/skills/vector-mind-autopilot/references/universal-system-prompt.md +24 -16
@@ -1,18 +1,224 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
- import { CompleteRequirementArgsSchema, GetPendingChangesArgsSchema, MAX_PENDING_LIMIT, PreflightChangeScopeArgsSchema, StartRequirementArgsSchema, SyncChangeIntentArgsSchema } from "../tool-schemas.js";
3
- import { buildDevelopmentWarnings, buildRequirementMappingWarnings, buildRequirementScopeContract, buildRequirementStartWarnings, buildScopeDriftWarnings, getRequirementItems, getRequirementScopeContract, isDevelopmentWarningBlockingForChangeMode, mergeScopeContracts, normalizeRequirementItems } from "../development-warnings.js";
3
+ import { CompleteRequirementArgsSchema, GetPendingChangesArgsSchema, PreflightChangeScopeArgsSchema, StartRequirementArgsSchema, SyncChangeIntentArgsSchema } from "../tool-schemas.js";
4
+ import { buildDevelopmentWarnings, buildRequirementMappingWarnings, buildRequirementScopeContract, buildRequirementStartWarnings, buildScopeDriftWarnings, getRequirementItems, getRequirementScopeContract, isDevelopmentWarningBlockingForChangeMode, mergeScopeContracts, normalizeRequirementItems, scopeContractHasRules } from "../development-warnings.js";
4
5
  import { mergePendingWithGit } from "../pending-changes.js";
5
6
  import { toolCompactOrJson } from "../token-savings.js";
6
7
  import { flushPendingChangeBuffer, indexFile } from "../file-indexing.js";
7
8
  import { buildFixPatternContent, buildFixPatternMetadata, buildFixPatternQualitySignals, collectRelevantFixPatterns, normalizeFixPattern } from "../fix-patterns.js";
8
9
  import { completeAllActiveRequirementMemoryItems, completeRequirementMemoryItemsByReqId } from "../memory-mutations.js";
9
- import { makePreviewText } from "../memory-recall.js";
10
+ import { makePreviewText, parseMetadataJson } from "../memory-recall.js";
10
11
  import { logActivity } from "../activity-log.js";
11
12
  import { compactPreflightChangeScopeText, safeJson, toolJson } from "../tool-output.js";
13
+ import { normalizeRequirementGoalIdentity, sameRequirement } from "../context-governance.js";
14
+ import { DEVELOPMENT_HUGE_FILE_LINES } from "../config.js";
15
+ import { hashFileContentStreaming } from "../large-file-split.js";
16
+ import { resolvePathWithinRoot } from "../path-containment.js";
17
+ const UNFINISHED_SPLIT_PLAN_STATUSES = new Set(["planned", "in_progress", "partial", "needs_refinement"]);
18
+ const MAX_SYNC_RESPONSE_ITEMS = 100;
19
+ const MAX_SYNC_RESPONSE_ARRAY_CHARS = 12_000;
20
+ const preflightResultCache = new Map();
21
+ function normalizeIdentityText(value) {
22
+ return (value ?? "").normalize("NFKC").trim().replace(/\s+/g, " ").toLocaleLowerCase();
23
+ }
24
+ function canonicalizeIdentity(value) {
25
+ if (Array.isArray(value))
26
+ return value.map(canonicalizeIdentity);
27
+ if (!value || typeof value !== "object")
28
+ return value;
29
+ return Object.fromEntries(Object.entries(value)
30
+ .filter(([, item]) => item !== undefined)
31
+ .sort(([left], [right]) => left.localeCompare(right))
32
+ .map(([key, item]) => [key, canonicalizeIdentity(item)]));
33
+ }
34
+ function sortedUniqueStrings(values) {
35
+ return [...new Set(values.map((value) => normalizeIdentityText(value)).filter(Boolean))].sort();
36
+ }
37
+ function createSyncResponseBudget() {
38
+ let remainingChars = MAX_SYNC_RESPONSE_ARRAY_CHARS;
39
+ return function boundItems(items) {
40
+ const page = [];
41
+ for (const item of items) {
42
+ if (page.length >= MAX_SYNC_RESPONSE_ITEMS)
43
+ break;
44
+ const itemChars = JSON.stringify(item).length + (page.length ? 1 : 0);
45
+ if (itemChars > remainingChars)
46
+ break;
47
+ page.push(item);
48
+ remainingChars -= itemChars;
49
+ }
50
+ return { page, total: items.length, truncated: page.length < items.length };
51
+ };
52
+ }
53
+ function rememberPreflight(key, value) {
54
+ preflightResultCache.set(key, value);
55
+ if (preflightResultCache.size <= 200)
56
+ return;
57
+ const oldest = preflightResultCache.keys().next().value;
58
+ if (oldest)
59
+ preflightResultCache.delete(oldest);
60
+ }
61
+ function renderPreflight(value, format) {
62
+ return {
63
+ content: [{
64
+ type: "text",
65
+ text: toolCompactOrJson("preflight_change_scope", value, compactPreflightChangeScopeText(value), format),
66
+ }],
67
+ };
68
+ }
69
+ function fileReachesLineThreshold(absPath, threshold) {
70
+ const fd = fs.openSync(absPath, "r");
71
+ try {
72
+ const buffer = Buffer.allocUnsafe(256 * 1024);
73
+ let lines = 0;
74
+ let bytesRead = 0;
75
+ let sawBytes = false;
76
+ let lastByte = 0;
77
+ do {
78
+ bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
79
+ if (bytesRead <= 0)
80
+ break;
81
+ sawBytes = true;
82
+ lastByte = buffer[bytesRead - 1];
83
+ for (let index = 0; index < bytesRead; index += 1) {
84
+ if (buffer[index] === 10)
85
+ lines += 1;
86
+ }
87
+ if (lines >= threshold)
88
+ return true;
89
+ } while (bytesRead > 0);
90
+ return lines + (sawBytes && lastByte !== 10 ? 1 : 0) >= threshold;
91
+ }
92
+ finally {
93
+ fs.closeSync(fd);
94
+ }
95
+ }
96
+ function hasUnfinishedLargeFileSplitPlan(context, reqId, filePath) {
97
+ const rows = context.getDb().prepare(`SELECT metadata_json
98
+ FROM memory_items
99
+ WHERE kind = 'large_file_split_plan' AND req_id = ? AND file_path = ?
100
+ ORDER BY updated_at DESC, id DESC
101
+ LIMIT 20`).all(reqId, filePath);
102
+ return rows.some((row) => UNFINISHED_SPLIT_PLAN_STATUSES.has(String(parseMetadataJson(row.metadata_json).status)));
103
+ }
104
+ function resolveActiveRequirement(context, args) {
105
+ const { listActiveRequirementsStmt } = context.getStatements();
106
+ const activeCount = Number(context.getDb().prepare(`SELECT COUNT(*) AS count FROM requirements WHERE status = 'active'`).get()?.count ?? 0);
107
+ if (args.req_id) {
108
+ const requirement = context.getDb().prepare(`SELECT id, title, status, context_data, goal_key, created_at
109
+ FROM requirements WHERE id = ? LIMIT 1`).get(args.req_id);
110
+ return {
111
+ requirement: requirement?.status === "superseded" ? undefined : requirement,
112
+ ambiguous: false,
113
+ active_count: activeCount,
114
+ requested_status: requirement?.status ?? null,
115
+ found: !!requirement,
116
+ };
117
+ }
118
+ if (args.goal_key?.trim()) {
119
+ const requirement = context.getDb().prepare(`SELECT id, title, status, context_data, goal_key, created_at
120
+ FROM requirements WHERE goal_key = ?
121
+ ORDER BY CASE status WHEN 'active' THEN 0 WHEN 'completed' THEN 1 ELSE 2 END,
122
+ updated_at DESC, id DESC
123
+ LIMIT 1`).get(args.goal_key.trim());
124
+ return {
125
+ requirement: requirement?.status === "superseded" ? undefined : requirement,
126
+ ambiguous: false,
127
+ active_count: activeCount,
128
+ requested_status: requirement?.status ?? null,
129
+ found: !!requirement,
130
+ };
131
+ }
132
+ const active = listActiveRequirementsStmt.all(2);
133
+ return {
134
+ requirement: active.length === 1 ? active[0] : undefined,
135
+ ambiguous: activeCount > 1,
136
+ active_count: activeCount,
137
+ requested_status: active.length === 1 ? active[0].status : null,
138
+ found: active.length === 1,
139
+ };
140
+ }
141
+ async function validateLargeFileSplitPlans(context, args, requirement, hugeFiles) {
142
+ const normalizeToDbPath = context.normalizeToDbPath;
143
+ const requiredFiles = [...new Set(hugeFiles.map(normalizeToDbPath))];
144
+ const ids = [...new Set([...(args.split_plan_ids ?? []), ...(args.split_plan_id ? [args.split_plan_id] : [])])];
145
+ const validByFile = new Map();
146
+ const invalid = [];
147
+ const currentHashes = new Map();
148
+ for (const planId of ids) {
149
+ const row = context.getStatements().getMemoryItemByIdStmt.get(planId);
150
+ if (!row || row.kind !== "large_file_split_plan") {
151
+ invalid.push({ plan_id: planId, reason: "not_a_large_file_split_plan" });
152
+ continue;
153
+ }
154
+ const metadata = parseMetadataJson(row.metadata_json);
155
+ const legacyPlan = metadata.plan && typeof metadata.plan === "object" && !Array.isArray(metadata.plan)
156
+ ? metadata.plan
157
+ : {};
158
+ const legacyCoverage = legacyPlan.coverage && typeof legacyPlan.coverage === "object" && !Array.isArray(legacyPlan.coverage)
159
+ ? legacyPlan.coverage
160
+ : {};
161
+ const legacyModuleConstraints = legacyPlan.module_constraints && typeof legacyPlan.module_constraints === "object" && !Array.isArray(legacyPlan.module_constraints)
162
+ ? legacyPlan.module_constraints
163
+ : {};
164
+ const file = typeof metadata.file === "string" ? normalizeToDbPath(metadata.file) : "";
165
+ const status = typeof metadata.status === "string" ? metadata.status : "";
166
+ const expectedHash = typeof metadata.current_state_hash === "string"
167
+ ? metadata.current_state_hash
168
+ : typeof metadata.source_state_hash === "string"
169
+ ? metadata.source_state_hash
170
+ : null;
171
+ let currentHash = null;
172
+ if (file) {
173
+ currentHash = currentHashes.get(file) ?? null;
174
+ if (!currentHash) {
175
+ const absPath = path.join(context.getProjectRoot(), file);
176
+ try {
177
+ currentHash = fs.statSync(absPath).isFile()
178
+ ? await hashFileContentStreaming(absPath)
179
+ : context.sha256Hex("missing");
180
+ }
181
+ catch {
182
+ currentHash = context.sha256Hex("missing");
183
+ }
184
+ currentHashes.set(file, currentHash);
185
+ }
186
+ }
187
+ let reason = "";
188
+ if (!requirement || row.req_id !== requirement.id)
189
+ reason = "requirement_mismatch";
190
+ else if (!file || !requiredFiles.includes(file))
191
+ reason = "file_mismatch";
192
+ else if (!["planned", "in_progress", "partial"].includes(status))
193
+ reason = `invalid_status:${status || "missing"}`;
194
+ else if ((metadata.plan_ok ?? legacyPlan.ok) !== true)
195
+ reason = "plan_requires_refinement";
196
+ else if ((metadata.coverage_complete ?? legacyCoverage.complete) !== true)
197
+ reason = "incomplete_declaration_coverage";
198
+ else if ((metadata.module_constraints_satisfied ?? legacyModuleConstraints.satisfied ?? false) !== true)
199
+ reason = "oversized_target_module";
200
+ else if (!expectedHash || expectedHash !== currentHash)
201
+ reason = "source_state_changed_since_plan";
202
+ if (reason) {
203
+ invalid.push({ plan_id: planId, reason });
204
+ continue;
205
+ }
206
+ validByFile.set(file, planId);
207
+ }
208
+ const missingFiles = requiredFiles.filter((file) => !validByFile.has(file));
209
+ return {
210
+ valid: requiredFiles.length > 0 && missingFiles.length === 0,
211
+ required_files: requiredFiles,
212
+ valid_plan_ids: [...validByFile.values()],
213
+ invalid,
214
+ missing_files: missingFiles,
215
+ };
216
+ }
12
217
  export async function handleStartRequirement(rawArgs, context) {
13
218
  const sha256Hex = context.sha256Hex;
14
- const { insertRequirementStmt, completeAllActiveRequirementsStmt, insertMemoryItemStmt } = context.getStatements();
219
+ const { insertRequirementStmt, insertMemoryItemStmt, getActiveRequirementByGoalKeyStmt, listActiveRequirementsStmt, getRequirementMemoryItemIdStmt, } = context.getStatements();
15
220
  const args = StartRequirementArgsSchema.parse(rawArgs);
221
+ const shouldClosePrevious = args.close_previous || args.previous_req_id != null;
16
222
  flushPendingChangeBuffer();
17
223
  const scope_contract = buildRequirementScopeContract({
18
224
  title: args.title,
@@ -26,27 +232,153 @@ export async function handleStartRequirement(rawArgs, context) {
26
232
  const development_warnings = buildRequirementStartWarnings({
27
233
  title: args.title,
28
234
  background: args.background,
29
- close_previous: args.close_previous,
235
+ close_previous: shouldClosePrevious,
30
236
  });
31
- if (args.close_previous) {
32
- try {
33
- completeAllActiveRequirementsStmt.run();
34
- completeAllActiveRequirementMemoryItems();
35
- }
36
- catch (err) {
37
- console.error("[vectormind] failed to close previous active requirements:", err);
38
- }
39
- }
40
- const info = insertRequirementStmt.run(args.title, args.background || null);
41
- const id = Number(info.lastInsertRowid);
237
+ const explicitGoalKey = args.goal_key?.trim() ?? "";
238
+ const goalKey = explicitGoalKey ||
239
+ `auto:${sha256Hex(normalizeRequirementGoalIdentity(args.title, args.background)).slice(0, 24)}`;
42
240
  const background = args.background?.trim() ?? "";
43
241
  const content = background ? `${args.title}\n\n${background}` : args.title;
44
- const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active", scope_contract, requirement_items }), sha256Hex(content));
45
- const memory_id = Number(memoryInfo.lastInsertRowid);
242
+ let writeOutcome;
243
+ try {
244
+ const writeTransaction = context.getDb().transaction(() => {
245
+ const currentByGoalKey = getActiveRequirementByGoalKeyStmt.get(goalKey);
246
+ const currentActiveCandidates = listActiveRequirementsStmt.all(50);
247
+ const currentExactActive = !explicitGoalKey
248
+ ? currentActiveCandidates.find((candidate) => sameRequirement(candidate, { ...args, goal_key: goalKey }))
249
+ : undefined;
250
+ const currentActive = currentByGoalKey ?? currentExactActive;
251
+ const currentReuseReason = currentByGoalKey
252
+ ? "goal_key"
253
+ : currentExactActive
254
+ ? "same_requirement"
255
+ : null;
256
+ if (args.reuse_active && currentActive && currentReuseReason) {
257
+ return {
258
+ kind: "reused",
259
+ requirement: currentActive,
260
+ reuseReason: currentReuseReason,
261
+ closedPrevious: false,
262
+ };
263
+ }
264
+ if (shouldClosePrevious && !args.previous_req_id && currentActiveCandidates.length > 1) {
265
+ return {
266
+ kind: "error",
267
+ error: "Multiple active requirements exist. Pass previous_req_id to close one, or pass an existing goal_key to resume it.",
268
+ activeRequirements: currentActiveCandidates.slice(0, 10),
269
+ activeCount: currentActiveCandidates.length,
270
+ };
271
+ }
272
+ let closedPrevious = false;
273
+ const previousRequirementId = args.previous_req_id ??
274
+ (currentActiveCandidates.length === 1 ? currentActiveCandidates[0].id : null);
275
+ if (shouldClosePrevious && previousRequirementId) {
276
+ const completed = completeRequirementMemoryItemsByReqId(previousRequirementId);
277
+ if (!completed) {
278
+ const raced = getActiveRequirementByGoalKeyStmt.get(goalKey);
279
+ if (raced) {
280
+ return {
281
+ kind: "reused",
282
+ requirement: raced,
283
+ reuseReason: "concurrent_goal_key_insert",
284
+ closedPrevious: false,
285
+ };
286
+ }
287
+ return {
288
+ kind: "error",
289
+ error: `previous_req_id ${previousRequirementId} is not active and cannot be closed.`,
290
+ };
291
+ }
292
+ closedPrevious = true;
293
+ }
294
+ let id;
295
+ try {
296
+ const info = insertRequirementStmt.run(args.title, args.background || null, goalKey);
297
+ id = Number(info.lastInsertRowid);
298
+ }
299
+ catch (err) {
300
+ const raced = getActiveRequirementByGoalKeyStmt.get(goalKey);
301
+ if (!raced)
302
+ throw err;
303
+ return {
304
+ kind: "reused",
305
+ requirement: raced,
306
+ reuseReason: "concurrent_goal_key_insert",
307
+ closedPrevious,
308
+ };
309
+ }
310
+ const memoryInfo = insertMemoryItemStmt.run("requirement", args.title, content, null, null, null, id, safeJson({ status: "active", goal_key: goalKey, scope_contract, requirement_items }), sha256Hex(content));
311
+ return {
312
+ kind: "created",
313
+ id,
314
+ memoryId: Number(memoryInfo.lastInsertRowid),
315
+ closedPrevious,
316
+ };
317
+ });
318
+ writeOutcome = writeTransaction.immediate();
319
+ }
320
+ catch (err) {
321
+ return {
322
+ isError: true,
323
+ content: [{
324
+ type: "text",
325
+ text: toolJson({ ok: false, error: `Failed to start requirement atomically: ${String(err)}` }),
326
+ }],
327
+ };
328
+ }
329
+ if (writeOutcome.kind === "error") {
330
+ return {
331
+ isError: true,
332
+ content: [{
333
+ type: "text",
334
+ text: toolJson({
335
+ ok: false,
336
+ error: writeOutcome.error,
337
+ ...(writeOutcome.activeRequirements
338
+ ? {
339
+ active_count: writeOutcome.activeCount ?? writeOutcome.activeRequirements.length,
340
+ active_requirements: writeOutcome.activeRequirements.map((candidate) => ({
341
+ id: candidate.id,
342
+ title: candidate.title,
343
+ goal_key: candidate.goal_key,
344
+ })),
345
+ }
346
+ : {}),
347
+ }),
348
+ }],
349
+ };
350
+ }
351
+ if (writeOutcome.kind === "reused") {
352
+ const memoryId = getRequirementMemoryItemIdStmt.get(writeOutcome.requirement.id)?.id ?? null;
353
+ const activeScopeContract = getRequirementScopeContract(writeOutcome.requirement.id);
354
+ const activeRequirementItems = getRequirementItems(writeOutcome.requirement.id);
355
+ logActivity("start_requirement", {
356
+ req_id: writeOutcome.requirement.id,
357
+ title: writeOutcome.requirement.title,
358
+ reused_active: true,
359
+ });
360
+ return {
361
+ content: [{
362
+ type: "text",
363
+ text: toolJson({
364
+ ok: true,
365
+ requirement: { id: writeOutcome.requirement.id, title: writeOutcome.requirement.title },
366
+ goal_key: writeOutcome.requirement.goal_key ?? goalKey,
367
+ memory_item: { id: memoryId },
368
+ reused: true,
369
+ reuse_reason: writeOutcome.reuseReason,
370
+ closed_previous: writeOutcome.closedPrevious,
371
+ scope_contract: activeScopeContract,
372
+ requirement_items: activeRequirementItems,
373
+ development_warnings: [],
374
+ }),
375
+ }],
376
+ };
377
+ }
46
378
  logActivity("start_requirement", {
47
- req_id: id,
379
+ req_id: writeOutcome.id,
48
380
  title: args.title,
49
- closed_previous: args.close_previous,
381
+ closed_previous: writeOutcome.closedPrevious,
50
382
  scope_contract,
51
383
  requirement_items,
52
384
  development_warnings: development_warnings.length,
@@ -57,9 +389,11 @@ export async function handleStartRequirement(rawArgs, context) {
57
389
  type: "text",
58
390
  text: toolJson({
59
391
  ok: true,
60
- requirement: { id, title: args.title },
61
- memory_item: { id: memory_id },
62
- closed_previous: args.close_previous,
392
+ requirement: { id: writeOutcome.id, title: args.title },
393
+ goal_key: goalKey,
394
+ memory_item: { id: writeOutcome.memoryId },
395
+ closed_previous: writeOutcome.closedPrevious,
396
+ close_previous_ignored: false,
63
397
  scope_contract,
64
398
  requirement_items,
65
399
  development_warnings,
@@ -70,12 +404,13 @@ export async function handleStartRequirement(rawArgs, context) {
70
404
  }
71
405
  export async function handlePreflightChangeScope(rawArgs, context) {
72
406
  const normalizeToDbPath = context.normalizeToDbPath;
73
- const { getActiveRequirementStmt } = context.getStatements();
74
407
  const args = PreflightChangeScopeArgsSchema.parse(rawArgs);
75
408
  const changeMode = args.change_mode;
76
409
  flushPendingChangeBuffer();
77
410
  const files = (args.files ?? args.planned_files ?? []).filter((f) => typeof f === "string" && f.length > 0);
78
- const active = getActiveRequirementStmt.get();
411
+ const resolution = resolveActiveRequirement(context, args);
412
+ const active = resolution.requirement;
413
+ const normalizedFiles = files.map(normalizeToDbPath);
79
414
  const explicitContract = buildRequirementScopeContract({
80
415
  title: active?.title ?? "",
81
416
  background: active?.context_data ?? "",
@@ -84,6 +419,20 @@ export async function handlePreflightChangeScope(rawArgs, context) {
84
419
  allowed_paths: args.allowed_paths,
85
420
  denied_paths: args.denied_paths,
86
421
  });
422
+ const persistedContract = active ? getRequirementScopeContract(active.id) : null;
423
+ const mergedContract = mergeScopeContracts(persistedContract, explicitContract);
424
+ const temporaryExactFileContract = !scopeContractHasRules(mergedContract) &&
425
+ args.files === undefined &&
426
+ (args.planned_files?.length ?? 0) > 0
427
+ ? {
428
+ allow_terms: [],
429
+ deny_terms: [],
430
+ allowed_paths: Array.from(new Set(normalizedFiles)),
431
+ denied_paths: [],
432
+ inferred_from: ["preflight.planned_files"],
433
+ }
434
+ : null;
435
+ const scope_contract = mergeScopeContracts(mergedContract, temporaryExactFileContract);
87
436
  const explicitRequirementItems = normalizeRequirementItems(args.requirement_items);
88
437
  const requirementItems = explicitRequirementItems.length
89
438
  ? explicitRequirementItems
@@ -95,7 +444,7 @@ export async function handlePreflightChangeScope(rawArgs, context) {
95
444
  ...buildDevelopmentWarnings(fileInputs, { includeUnspecified: fileInputs.length === 0 }),
96
445
  ...buildScopeDriftWarnings({
97
446
  requirement: active,
98
- contract: explicitContract,
447
+ contract: scope_contract,
99
448
  intent: args.intent,
100
449
  files: fileInputs,
101
450
  includeMissingContractHint: true,
@@ -108,22 +457,22 @@ export async function handlePreflightChangeScope(rawArgs, context) {
108
457
  change_mode: changeMode,
109
458
  }),
110
459
  ];
111
- const scope_contract = mergeScopeContracts(active ? getRequirementScopeContract(active.id) : null, explicitContract);
112
- logActivity("preflight_change_scope", {
113
- req_id: active?.id ?? null,
114
- intent_preview: makePreviewText(args.intent, 200),
115
- change_mode: changeMode,
116
- files: files.slice(0, 25),
117
- files_total: files.length,
118
- development_warnings: development_warnings.length,
119
- });
120
460
  const hasTargetFiles = fileInputs.length > 0;
121
461
  const hugeWarnings = development_warnings.filter((w) => w.code === "huge_file_modularization_required");
122
462
  const hasHugeFile = hugeWarnings.length > 0;
123
- const blockingWarnings = development_warnings.filter((w) => isDevelopmentWarningBlockingForChangeMode(w, changeMode));
124
- const hasBlockingWarnings = blockingWarnings.length > 0;
125
- const safeToEdit = hasTargetFiles && !hasBlockingWarnings;
126
- const normalizedFiles = files.map(normalizeToDbPath);
463
+ const splitPlanValidation = await validateLargeFileSplitPlans(context, args, active, hugeWarnings.flatMap((warning) => warning.files ?? []));
464
+ const deferReason = args.defer_split_reason?.trim() ?? "";
465
+ const minimalBugfixAllowed = changeMode === "bugfix" &&
466
+ args.adds_responsibility === false &&
467
+ deferReason.length > 0;
468
+ const emergencyHotfixAllowed = changeMode === "emergency_hotfix" && deferReason.length > 0;
469
+ const mechanicalPlanAllowed = changeMode === "mechanical_modularization" && splitPlanValidation.valid;
470
+ const hugeGateBlocked = hasHugeFile && !minimalBugfixAllowed && !emergencyHotfixAllowed && !mechanicalPlanAllowed;
471
+ const blockingWarnings = development_warnings.filter((warning) => warning.code !== "huge_file_modularization_required" &&
472
+ isDevelopmentWarningBlockingForChangeMode(warning, changeMode));
473
+ const hasBlockingWarnings = blockingWarnings.length > 0 || hugeGateBlocked;
474
+ const hasActiveRequirement = !!active;
475
+ const safeToEdit = hasActiveRequirement && hasTargetFiles && !hasBlockingWarnings;
127
476
  const relevantFixPatterns = collectRelevantFixPatterns(context, {
128
477
  intent: args.intent,
129
478
  files: normalizedFiles,
@@ -132,31 +481,62 @@ export async function handlePreflightChangeScope(rawArgs, context) {
132
481
  limit: 3,
133
482
  });
134
483
  const quality_signals = buildFixPatternQualitySignals(relevantFixPatterns);
135
- const recommendedAction = !hasTargetFiles
136
- ? "Identify the intended target files/modules and rerun preflight_change_scope before editing."
137
- : hasHugeFile && changeMode === "mechanical_modularization" && safeToEdit
138
- ? "Proceed only with mechanical modularization: call plan_large_file_split, move whole declarations into real named modules/directories, avoid generated/parts files, validate, then record_large_file_split."
139
- : hasHugeFile && changeMode === "emergency_hotfix" && safeToEdit
140
- ? "Proceed only with the smallest urgent fix, do not add new responsibilities, record why mechanical modularization was deferred, and plan/record the split next."
141
- : hasHugeFile
142
- ? "Stop normal feature work. Call plan_large_file_split and perform mechanical modularization first, or rerun preflight_change_scope with change_mode='mechanical_modularization' for the split itself."
143
- : hasBlockingWarnings
144
- ? "Stop before editing. Narrow the planned files or explicitly expand the current requirement/scope contract."
145
- : "Planned files are within the current generic scope checks.";
146
- const requiredAction = hasHugeFile && changeMode !== "mechanical_modularization"
147
- ? "mechanical_modularization"
484
+ const recommendedAction = resolution.ambiguous
485
+ ? "Multiple active requirements exist. Pass req_id or goal_key so this preflight cannot attach to another task."
486
+ : !hasActiveRequirement
487
+ ? "Start or resume the current code-change requirement before editing, or pass its req_id/goal_key."
488
+ : !hasTargetFiles
489
+ ? "Identify the intended target files/modules and rerun preflight_change_scope before editing."
490
+ : hasHugeFile && changeMode === "mechanical_modularization" && !splitPlanValidation.valid
491
+ ? "Mechanical modularization requires a valid split_plan_id for every huge target file, with complete declaration coverage and an unchanged source state."
492
+ : hasHugeFile && changeMode === "mechanical_modularization" && safeToEdit
493
+ ? "Proceed with the persisted split plan, move whole declarations into real named modules/directories, validate, then update that plan with record_large_file_split."
494
+ : hasHugeFile && changeMode === "bugfix" && minimalBugfixAllowed && safeToEdit
495
+ ? "Proceed only with the declared minimal bugfix, add no responsibilities, and keep the split deferral reason in the final change intent."
496
+ : hasHugeFile && changeMode === "emergency_hotfix" && safeToEdit
497
+ ? "Proceed only with the smallest urgent fix, add no responsibilities, record why modularization was deferred, and schedule the split next."
498
+ : hasHugeFile
499
+ ? "Stop normal feature editing. Call plan_large_file_split and perform mechanical modularization first, then rerun preflight_change_scope with change_mode='mechanical_modularization'."
500
+ : hasBlockingWarnings
501
+ ? "Stop before editing. Narrow the planned files or explicitly expand the current requirement/scope contract."
502
+ : "Planned files are within the current generic scope checks.";
503
+ const requiredAction = hasHugeFile && hugeGateBlocked
504
+ ? changeMode === "mechanical_modularization"
505
+ ? "valid_split_plan"
506
+ : "mechanical_modularization"
148
507
  : undefined;
149
508
  const allowedChangeModes = hasHugeFile
150
- ? ["mechanical_modularization", "emergency_hotfix"]
509
+ ? ["mechanical_modularization", "bugfix", "emergency_hotfix"]
151
510
  : undefined;
152
- const outputValue = {
511
+ const baseOutputValue = {
153
512
  ok: safeToEdit,
154
513
  safe_to_edit: safeToEdit,
514
+ advisory_only: !hasHugeFile,
515
+ workflow_gate: hasHugeFile
516
+ ? {
517
+ code: "huge_file_modularization_required",
518
+ active: hasBlockingWarnings,
519
+ satisfied: !hasBlockingWarnings,
520
+ required_action: requiredAction ?? changeMode,
521
+ host_runtime_enforced: false,
522
+ minimal_bugfix_allowed: minimalBugfixAllowed,
523
+ emergency_hotfix_allowed: emergencyHotfixAllowed,
524
+ }
525
+ : null,
155
526
  change_mode: changeMode,
156
527
  recommended_action: recommendedAction,
157
528
  required_action: requiredAction,
158
529
  allowed_change_modes: allowedChangeModes,
530
+ split_plan_validation: splitPlanValidation,
531
+ adds_responsibility: args.adds_responsibility ?? null,
532
+ defer_split_reason: deferReason || null,
159
533
  active_requirement: active ? { id: active.id, title: active.title } : null,
534
+ requirement_resolution: {
535
+ requested_req_id: args.req_id ?? null,
536
+ requested_goal_key: args.goal_key ?? null,
537
+ ambiguous: resolution.ambiguous,
538
+ active_count: resolution.active_count,
539
+ },
160
540
  intent: args.intent,
161
541
  files: normalizedFiles,
162
542
  requirement_mapping: {
@@ -164,17 +544,37 @@ export async function handlePreflightChangeScope(rawArgs, context) {
164
544
  planned_changes: args.planned_changes ?? [],
165
545
  },
166
546
  scope_contract,
547
+ scope_contract_persistence: temporaryExactFileContract
548
+ ? "temporary_read_only"
549
+ : scopeContractHasRules(scope_contract)
550
+ ? "persisted_or_explicit"
551
+ : "none",
167
552
  development_warnings,
168
553
  quality_signals,
169
554
  };
170
- return {
171
- content: [
172
- {
173
- type: "text",
174
- text: toolCompactOrJson("preflight_change_scope", outputValue, compactPreflightChangeScopeText(outputValue), args.format),
175
- },
176
- ],
555
+ const preflightIdempotencyKey = context.sha256Hex(JSON.stringify(canonicalizeIdentity(baseOutputValue)));
556
+ const cacheKey = `${context.getProjectRoot()}\n${preflightIdempotencyKey}`;
557
+ const cachedPreflight = preflightResultCache.get(cacheKey);
558
+ const reused = !!cachedPreflight;
559
+ const outputValue = {
560
+ ...(cachedPreflight ?? baseOutputValue),
561
+ reused,
562
+ idempotency_key: preflightIdempotencyKey,
177
563
  };
564
+ if (!cachedPreflight) {
565
+ rememberPreflight(cacheKey, outputValue);
566
+ }
567
+ logActivity("preflight_change_scope", {
568
+ req_id: active?.id ?? null,
569
+ intent_preview: makePreviewText(args.intent, 200),
570
+ change_mode: changeMode,
571
+ files: files.slice(0, 25),
572
+ files_total: files.length,
573
+ development_warnings: development_warnings.length,
574
+ reused,
575
+ idempotency_key: preflightIdempotencyKey,
576
+ });
577
+ return renderPreflight(outputValue, args.format);
178
578
  }
179
579
  export async function handleSyncChangeIntent(rawArgs, context) {
180
580
  const db = context.getDb();
@@ -182,7 +582,7 @@ export async function handleSyncChangeIntent(rawArgs, context) {
182
582
  const normalizeToDbPath = context.normalizeToDbPath;
183
583
  const sha256Hex = context.sha256Hex;
184
584
  const getFileStateHash = context.getFileStateHash;
185
- const { insertChangeLogStmt, insertMemoryItemStmt, getActiveRequirementStmt, listPendingChangesStmt, deletePendingChangeStmt, deleteAllPendingChangesStmt, } = context.getStatements();
585
+ const { insertChangeLogStmt, upsertSyncedFileStateStmt, insertMemoryItemStmt, listPendingChangesStmt, deletePendingChangeStmt, } = context.getStatements();
186
586
  const args = SyncChangeIntentArgsSchema.parse(rawArgs);
187
587
  const explicitFixPattern = args.fix_pattern
188
588
  ? normalizeFixPattern({
@@ -193,10 +593,146 @@ export async function handleSyncChangeIntent(rawArgs, context) {
193
593
  : args.verification_gaps,
194
594
  })
195
595
  : null;
596
+ const toContainedFile = (input, allowMissing) => {
597
+ const absolute = resolvePathWithinRoot(projectRoot, input, { allowMissing });
598
+ const relative = path.relative(projectRoot, absolute);
599
+ if (!relative || relative === ".") {
600
+ throw new Error(`sync_change_intent file must identify an entry below project_root: ${input}`);
601
+ }
602
+ return { absolute, dbFilePath: normalizeToDbPath(relative) };
603
+ };
604
+ const explicitFileInputs = [...(args.files ?? []), ...(args.affected_files ?? [])].filter((f) => typeof f === "string" && f.length > 0);
605
+ const explicitFiles = explicitFileInputs.map((rawFile) => {
606
+ const contained = toContainedFile(rawFile, true);
607
+ return { rawFile: contained.absolute, dbFilePath: contained.dbFilePath };
608
+ });
609
+ const largeFileSplitDeferrals = (args.large_file_split_deferrals ?? []).map((deferral) => {
610
+ const contained = toContainedFile(deferral.file, true);
611
+ return {
612
+ file: contained.dbFilePath,
613
+ reason: deferral.reason.trim(),
614
+ recorded_at: new Date().toISOString(),
615
+ };
616
+ });
617
+ const syncIdempotencyKey = args.idempotency_key?.trim() || null;
618
+ const requestFingerprint = syncIdempotencyKey
619
+ ? sha256Hex(JSON.stringify(canonicalizeIdentity({
620
+ req_id: args.req_id ?? null,
621
+ goal_key: normalizeIdentityText(args.goal_key),
622
+ intent: normalizeIdentityText(args.intent),
623
+ files: explicitFiles.map((file) => file.dbFilePath).sort(),
624
+ verification: sortedUniqueStrings(args.verification ?? []),
625
+ verification_gaps: sortedUniqueStrings(args.verification_gaps ?? []),
626
+ fix_pattern: explicitFixPattern,
627
+ large_file_split_deferrals: largeFileSplitDeferrals
628
+ .map((deferral) => ({ file: deferral.file, reason: normalizeIdentityText(deferral.reason) }))
629
+ .sort((left, right) => left.file.localeCompare(right.file)),
630
+ pending_limit: args.pending_limit,
631
+ complete_requirement: args.complete_requirement,
632
+ })))
633
+ : null;
634
+ const findExistingSync = () => {
635
+ if (!syncIdempotencyKey)
636
+ return undefined;
637
+ return db.prepare(`SELECT mi.id, mi.req_id, r.title AS requirement_title, mi.metadata_json
638
+ FROM memory_items mi
639
+ JOIN requirements r ON r.id = mi.req_id
640
+ WHERE mi.kind = 'change_intent'
641
+ AND json_valid(COALESCE(mi.metadata_json, '{}'))
642
+ AND json_extract(mi.metadata_json, '$.idempotency_key') = ?
643
+ ORDER BY mi.updated_at DESC, mi.id DESC
644
+ LIMIT 1`).get(syncIdempotencyKey);
645
+ };
646
+ const replayExistingSync = (existing) => {
647
+ const metadata = parseMetadataJson(existing.metadata_json);
648
+ if (metadata.request_fingerprint !== requestFingerprint) {
649
+ return {
650
+ isError: true,
651
+ content: [{
652
+ type: "text",
653
+ text: toolJson({
654
+ ok: false,
655
+ error: "idempotency_key conflict: the key was already used with different sync_change_intent arguments",
656
+ idempotency_key: syncIdempotencyKey,
657
+ }),
658
+ }],
659
+ };
660
+ }
661
+ const storedFiles = Array.isArray(metadata.files)
662
+ ? metadata.files
663
+ .filter((file) => !!file && typeof file === "object" && !Array.isArray(file))
664
+ .map((file) => ({
665
+ file_path: String(file.file_path ?? ""),
666
+ event: String(file.event ?? "manual"),
667
+ source: file.source === "args" || file.source === "pending" || file.source === "unspecified"
668
+ ? file.source
669
+ : "unspecified",
670
+ }))
671
+ .filter((file) => file.file_path.length > 0)
672
+ : [];
673
+ const changeLogId = Number(metadata.change_log_id ?? 0);
674
+ const developmentWarnings = buildDevelopmentWarnings(storedFiles, {
675
+ includeUnspecified: storedFiles.some((file) => file.file_path === "(unspecified)"),
676
+ });
677
+ const boundResponseItems = createSyncResponseBudget();
678
+ const boundedStoredFiles = boundResponseItems(storedFiles);
679
+ const storedVerification = Array.isArray(metadata.verification) ? metadata.verification : [];
680
+ const storedVerificationGaps = Array.isArray(metadata.verification_gaps) ? metadata.verification_gaps : [];
681
+ const storedDeferrals = Array.isArray(metadata.large_file_split_deferrals)
682
+ ? metadata.large_file_split_deferrals
683
+ : [];
684
+ const boundedVerification = boundResponseItems(storedVerification);
685
+ const boundedVerificationGaps = boundResponseItems(storedVerificationGaps);
686
+ const boundedDeferrals = boundResponseItems(storedDeferrals);
687
+ const boundedWarnings = boundResponseItems(developmentWarnings);
688
+ return {
689
+ content: [{
690
+ type: "text",
691
+ text: toolJson({
692
+ ok: true,
693
+ linked_to_requirement: { id: existing.req_id, title: existing.requirement_title },
694
+ synced_files: boundedStoredFiles.page,
695
+ synced_files_total: boundedStoredFiles.total,
696
+ synced_files_truncated: boundedStoredFiles.truncated,
697
+ created: [],
698
+ created_total: 0,
699
+ created_truncated: false,
700
+ created_change: {
701
+ change_log_id: changeLogId,
702
+ memory_item_id: existing.id,
703
+ file_count: Number(metadata.file_count ?? storedFiles.filter((file) => file.file_path !== "(unspecified)").length),
704
+ },
705
+ verification: boundedVerification.page,
706
+ verification_total: boundedVerification.total,
707
+ verification_truncated: boundedVerification.truncated,
708
+ verification_gaps: boundedVerificationGaps.page,
709
+ verification_gaps_total: boundedVerificationGaps.total,
710
+ verification_gaps_truncated: boundedVerificationGaps.truncated,
711
+ large_file_split_deferrals: boundedDeferrals.page,
712
+ large_file_split_deferrals_total: boundedDeferrals.total,
713
+ large_file_split_deferrals_truncated: boundedDeferrals.truncated,
714
+ created_fix_pattern: metadata.created_fix_pattern ?? null,
715
+ requirement_completed: metadata.complete_requirement === true,
716
+ pending_batch: metadata.pending_batch ?? null,
717
+ reused: true,
718
+ idempotency_key: syncIdempotencyKey,
719
+ development_warnings: boundedWarnings.page,
720
+ development_warnings_total: boundedWarnings.total,
721
+ development_warnings_truncated: boundedWarnings.truncated,
722
+ }),
723
+ }],
724
+ };
725
+ };
726
+ const existingBeforeResolution = findExistingSync();
727
+ if (existingBeforeResolution)
728
+ return replayExistingSync(existingBeforeResolution);
196
729
  flushPendingChangeBuffer();
197
- const explicitFiles = (args.files ?? args.affected_files ?? []).filter((f) => typeof f === "string" && f.length > 0);
198
- const active = getActiveRequirementStmt.get();
730
+ const resolution = resolveActiveRequirement(context, args);
731
+ const active = resolution.requirement;
199
732
  if (!active) {
733
+ const existingAfterResolution = findExistingSync();
734
+ if (existingAfterResolution)
735
+ return replayExistingSync(existingAfterResolution);
200
736
  return {
201
737
  isError: true,
202
738
  content: [
@@ -204,39 +740,84 @@ export async function handleSyncChangeIntent(rawArgs, context) {
204
740
  type: "text",
205
741
  text: toolJson({
206
742
  ok: false,
207
- error: "No active requirement. Call start_requirement({ project_root, title, background }) before syncing change intent.",
743
+ code: resolution.ambiguous
744
+ ? "MULTIPLE_ACTIVE_REQUIREMENTS"
745
+ : resolution.requested_status === "superseded"
746
+ ? "REQUIREMENT_SUPERSEDED"
747
+ : resolution.found
748
+ ? "REQUIREMENT_NOT_WRITABLE"
749
+ : "REQUIREMENT_NOT_ACTIVE_IN_PROJECT",
750
+ error: resolution.ambiguous
751
+ ? "Multiple active requirements exist. Pass req_id or goal_key to sync_change_intent."
752
+ : resolution.requested_status === "superseded"
753
+ ? "The requested requirement was superseded and cannot accept new change intent. Start a new requirement instead."
754
+ : "No matching active requirement exists under the resolved project root. Call start_requirement({ project_root, title, background }) for a new goal, or inspect/resume an existing requirement.",
755
+ project_root: context.getProjectRoot(),
756
+ requested: {
757
+ req_id: args.req_id ?? null,
758
+ goal_key: args.goal_key?.trim() || null,
759
+ },
760
+ active_count: resolution.active_count,
761
+ active_requirements: context.getStatements().listActiveRequirementsStmt.all(10)
762
+ .map((requirement) => ({ id: requirement.id, title: requirement.title, goal_key: requirement.goal_key })),
763
+ recovery: resolution.ambiguous
764
+ ? { action: "pass_requirement_identity", tools: ["get_requirement_status"] }
765
+ : resolution.requested_status === "superseded"
766
+ ? { action: "start_replacement_requirement", tools: ["get_requirement_status", "start_requirement"] }
767
+ : {
768
+ action: "inspect_or_resume_requirement",
769
+ tools: ["get_requirement_status", "resume_requirement", "start_requirement"],
770
+ hint: "Verify project_root matches the start_requirement call. Use project_root_mode=exact only for an intentionally independent nested project.",
771
+ },
208
772
  }),
209
773
  },
210
774
  ],
211
775
  };
212
776
  }
213
777
  const created = [];
778
+ let createdChange = null;
779
+ const reusedSync = false;
780
+ let pendingBatch = null;
214
781
  const fixPatternMemoryItem = { value: null };
215
782
  const synced_files = [];
783
+ let transactionReplay;
216
784
  const insertTx = db.transaction(() => {
785
+ transactionReplay = findExistingSync();
786
+ if (transactionReplay)
787
+ return;
788
+ const pendingPathsToDelete = new Set();
217
789
  const targets = [];
218
790
  if (explicitFiles.length) {
219
- for (const rawFile of explicitFiles) {
220
- const dbFilePath = normalizeToDbPath(rawFile);
221
- targets.push({ rawFile, dbFilePath, event: "manual", source: "args" });
222
- }
223
- for (const t of targets) {
224
- deletePendingChangeStmt.run(t.dbFilePath);
791
+ for (const file of explicitFiles) {
792
+ targets.push({ rawFile: file.rawFile, dbFilePath: file.dbFilePath, event: "manual", source: "args" });
793
+ pendingPathsToDelete.add(file.dbFilePath);
225
794
  }
226
795
  }
227
796
  else {
228
797
  const pendingAll = listPendingChangesStmt.all();
229
- const merged = mergePendingWithGit(pendingAll, { offset: 0, limit: MAX_PENDING_LIMIT });
798
+ const merged = mergePendingWithGit(pendingAll, { offset: 0, limit: args.pending_limit });
799
+ pendingBatch = {
800
+ limit: args.pending_limit,
801
+ total: merged.total,
802
+ processed: merged.page.length,
803
+ remaining: merged.remaining,
804
+ truncated: merged.truncated,
805
+ };
806
+ if (args.complete_requirement && merged.remaining > 0) {
807
+ throw new Error(`Cannot complete requirement while ${merged.remaining} pending change(s) remain after this bounded batch. ` +
808
+ "Run sync_change_intent with complete_requirement=false until pending_batch.remaining is 0, then complete it.");
809
+ }
230
810
  if (merged.page.length) {
231
811
  for (const p of merged.page) {
812
+ const contained = toContainedFile(p.file_path, p.last_event === "unlink");
232
813
  targets.push({
233
- rawFile: p.file_path,
234
- dbFilePath: p.file_path,
814
+ rawFile: contained.absolute,
815
+ dbFilePath: contained.dbFilePath,
235
816
  event: p.last_event,
236
- source: p.source === "git" ? "pending" : "pending",
817
+ source: "pending",
237
818
  });
819
+ pendingPathsToDelete.add(p.file_path);
238
820
  }
239
- deleteAllPendingChangesStmt.run();
240
821
  }
241
822
  else {
242
823
  targets.push({
@@ -247,31 +828,108 @@ export async function handleSyncChangeIntent(rawArgs, context) {
247
828
  });
248
829
  }
249
830
  }
250
- for (const t of targets) {
831
+ const uniqueTargets = [];
832
+ const seenTargetPaths = new Set();
833
+ for (const target of targets) {
834
+ const key = target.dbFilePath === "(unspecified)"
835
+ ? "(unspecified)"
836
+ : process.platform === "win32"
837
+ ? target.dbFilePath.toLowerCase()
838
+ : target.dbFilePath;
839
+ if (seenTargetPaths.has(key))
840
+ continue;
841
+ seenTargetPaths.add(key);
842
+ uniqueTargets.push(target);
843
+ }
844
+ const fileStates = uniqueTargets.map((t) => {
251
845
  const isUnspecified = t.dbFilePath === "(unspecified)";
252
- const changeInfo = insertChangeLogStmt.run(active.id, t.dbFilePath, args.intent);
253
- const change_log_id = Number(changeInfo.lastInsertRowid);
254
- const memoryInfo = insertMemoryItemStmt.run("change_intent", active.title, args.intent, isUnspecified ? null : t.dbFilePath, null, null, active.id, safeJson({
255
- change_log_id,
846
+ return {
847
+ rawFile: t.rawFile,
848
+ file_path: t.dbFilePath,
256
849
  event: t.event,
257
850
  source: t.source,
258
851
  file_state_hash: isUnspecified ? null : getFileStateHash(t.rawFile),
259
- verification: args.verification ?? [],
260
- verification_gaps: args.verification_gaps ?? [],
261
- }), sha256Hex(args.intent));
262
- const memory_item_id = Number(memoryInfo.lastInsertRowid);
263
- synced_files.push({ file_path: t.dbFilePath, event: t.event, source: t.source });
852
+ unspecified: isUnspecified,
853
+ };
854
+ });
855
+ const concreteFileStates = fileStates.filter((f) => !f.unspecified);
856
+ if (largeFileSplitDeferrals.length) {
857
+ const syncedPathKeys = new Set(concreteFileStates.map((file) => process.platform === "win32" ? file.file_path.toLowerCase() : file.file_path));
858
+ for (const deferral of largeFileSplitDeferrals) {
859
+ const deferralKey = process.platform === "win32" ? deferral.file.toLowerCase() : deferral.file;
860
+ if (!syncedPathKeys.has(deferralKey)) {
861
+ throw new Error(`Large-file split deferral must belong to this sync_change_intent file set: ${deferral.file}`);
862
+ }
863
+ const fileState = concreteFileStates.find((file) => (process.platform === "win32" ? file.file_path.toLowerCase() : file.file_path) === deferralKey);
864
+ let currentlyHuge = false;
865
+ if (fileState && fileState.event !== "unlink") {
866
+ const absPath = path.isAbsolute(fileState.rawFile)
867
+ ? fileState.rawFile
868
+ : path.join(projectRoot, fileState.rawFile);
869
+ try {
870
+ currentlyHuge = fs.statSync(absPath).isFile() && fileReachesLineThreshold(absPath, DEVELOPMENT_HUGE_FILE_LINES);
871
+ }
872
+ catch {
873
+ currentlyHuge = false;
874
+ }
875
+ }
876
+ if (!currentlyHuge && !hasUnfinishedLargeFileSplitPlan(context, active.id, deferral.file)) {
877
+ throw new Error(`Large-file split deferral requires a currently huge file or an unfinished split plan for this requirement: ${deferral.file}`);
878
+ }
879
+ }
880
+ }
881
+ for (const pendingPath of pendingPathsToDelete)
882
+ deletePendingChangeStmt.run(pendingPath);
883
+ const primaryFilePath = concreteFileStates.length === 1
884
+ ? concreteFileStates[0].file_path
885
+ : concreteFileStates.length > 1
886
+ ? "(multiple)"
887
+ : null;
888
+ const filesJson = safeJson(concreteFileStates.map((f) => ({
889
+ file_path: f.file_path,
890
+ event: f.event,
891
+ source: f.source,
892
+ file_state_hash: f.file_state_hash,
893
+ })));
894
+ const fileCount = concreteFileStates.length;
895
+ const changeInfo = insertChangeLogStmt.run(active.id, primaryFilePath, args.intent, filesJson, fileCount);
896
+ const change_log_id = Number(changeInfo.lastInsertRowid);
897
+ const memoryMetadata = safeJson({
898
+ change_log_id,
899
+ files: concreteFileStates.map((f) => ({
900
+ file_path: f.file_path,
901
+ event: f.event,
902
+ source: f.source,
903
+ file_state_hash: f.file_state_hash,
904
+ })),
905
+ file_count: fileCount,
906
+ verification: args.verification ?? [],
907
+ verification_gaps: args.verification_gaps ?? [],
908
+ large_file_split_deferrals: largeFileSplitDeferrals,
909
+ idempotency_key: syncIdempotencyKey,
910
+ request_fingerprint: requestFingerprint,
911
+ pending_batch: pendingBatch,
912
+ complete_requirement: args.complete_requirement,
913
+ });
914
+ const memoryInfo = insertMemoryItemStmt.run("change_intent", active.title, args.intent, concreteFileStates.length === 1 ? concreteFileStates[0].file_path : null, null, null, active.id, memoryMetadata, sha256Hex(`${args.intent}\n${concreteFileStates.map((f) => f.file_path).join("\n")}`));
915
+ const memory_item_id = Number(memoryInfo.lastInsertRowid);
916
+ createdChange = { change_log_id, memory_item_id, file_count: fileCount };
917
+ for (const f of fileStates) {
918
+ synced_files.push({ file_path: f.file_path, event: f.event, source: f.source });
264
919
  created.push({
265
- file_path: t.dbFilePath,
266
- event: t.event,
267
- source: t.source,
920
+ file_path: f.file_path,
921
+ event: f.event,
922
+ source: f.source,
268
923
  change_log_id,
269
924
  memory_item_id,
270
925
  });
271
- if (!isUnspecified && t.event !== "unlink") {
272
- const abs = path.isAbsolute(t.rawFile)
273
- ? t.rawFile
274
- : path.join(projectRoot, t.rawFile);
926
+ if (!f.unspecified) {
927
+ upsertSyncedFileStateStmt.run(f.file_path, f.file_state_hash, change_log_id);
928
+ }
929
+ if (!f.unspecified && f.event !== "unlink") {
930
+ const abs = path.isAbsolute(f.rawFile)
931
+ ? f.rawFile
932
+ : path.join(projectRoot, f.rawFile);
275
933
  indexFile(abs, "manual");
276
934
  }
277
935
  }
@@ -284,14 +942,29 @@ export async function handleSyncChangeIntent(rawArgs, context) {
284
942
  intent: args.intent,
285
943
  files: sourceFiles,
286
944
  requirement_id: active.id,
287
- source_change_ids: created.map((c) => c.change_log_id),
945
+ source_change_ids: createdChange ? [createdChange.change_log_id] : created.map((c) => c.change_log_id),
288
946
  verification: args.verification,
289
947
  verification_gaps: args.verification_gaps,
290
948
  }), sha256Hex(content));
291
949
  fixPatternMemoryItem.value = { id: Number(memoryInfo.lastInsertRowid), kind: "fix_pattern", title };
292
950
  }
951
+ if (args.complete_requirement) {
952
+ if (!completeRequirementMemoryItemsByReqId(active.id)) {
953
+ throw new Error(`Requirement ${active.id} is no longer active and cannot be completed.`);
954
+ }
955
+ }
293
956
  });
294
- insertTx();
957
+ try {
958
+ insertTx();
959
+ }
960
+ catch (err) {
961
+ return {
962
+ isError: true,
963
+ content: [{ type: "text", text: toolJson({ ok: false, error: `sync_change_intent transaction failed: ${String(err)}` }) }],
964
+ };
965
+ }
966
+ if (transactionReplay)
967
+ return replayExistingSync(transactionReplay);
295
968
  const development_warnings = [
296
969
  ...buildDevelopmentWarnings(synced_files, {
297
970
  includeUnspecified: synced_files.some((f) => f.file_path === "(unspecified)"),
@@ -302,6 +975,13 @@ export async function handleSyncChangeIntent(rawArgs, context) {
302
975
  files: synced_files,
303
976
  }),
304
977
  ];
978
+ const boundResponseItems = createSyncResponseBudget();
979
+ const boundedSyncedFiles = boundResponseItems(synced_files);
980
+ const boundedCreated = boundResponseItems(created);
981
+ const boundedVerification = boundResponseItems(args.verification ?? []);
982
+ const boundedVerificationGaps = boundResponseItems(args.verification_gaps ?? []);
983
+ const boundedDeferrals = boundResponseItems(largeFileSplitDeferrals);
984
+ const boundedWarnings = boundResponseItems(development_warnings);
305
985
  logActivity("sync_change_intent", {
306
986
  req_id: active.id,
307
987
  title: active.title,
@@ -310,6 +990,10 @@ export async function handleSyncChangeIntent(rawArgs, context) {
310
990
  files_total: synced_files.length,
311
991
  fix_pattern_memory_id: fixPatternMemoryItem.value?.id ?? null,
312
992
  development_warnings: development_warnings.length,
993
+ large_file_split_deferrals: largeFileSplitDeferrals.length,
994
+ pending_batch: pendingBatch,
995
+ reused: reusedSync,
996
+ idempotency_key: syncIdempotencyKey,
313
997
  });
314
998
  return {
315
999
  content: [
@@ -318,19 +1002,37 @@ export async function handleSyncChangeIntent(rawArgs, context) {
318
1002
  text: toolJson({
319
1003
  ok: true,
320
1004
  linked_to_requirement: { id: active.id, title: active.title },
321
- synced_files,
322
- created,
323
- verification: args.verification ?? [],
324
- verification_gaps: args.verification_gaps ?? [],
1005
+ synced_files: boundedSyncedFiles.page,
1006
+ synced_files_total: boundedSyncedFiles.total,
1007
+ synced_files_truncated: boundedSyncedFiles.truncated,
1008
+ created: boundedCreated.page,
1009
+ created_total: boundedCreated.total,
1010
+ created_truncated: boundedCreated.truncated,
1011
+ created_change: createdChange,
1012
+ verification: boundedVerification.page,
1013
+ verification_total: boundedVerification.total,
1014
+ verification_truncated: boundedVerification.truncated,
1015
+ verification_gaps: boundedVerificationGaps.page,
1016
+ verification_gaps_total: boundedVerificationGaps.total,
1017
+ verification_gaps_truncated: boundedVerificationGaps.truncated,
1018
+ large_file_split_deferrals: boundedDeferrals.page,
1019
+ large_file_split_deferrals_total: boundedDeferrals.total,
1020
+ large_file_split_deferrals_truncated: boundedDeferrals.truncated,
325
1021
  created_fix_pattern: fixPatternMemoryItem.value,
326
- development_warnings,
1022
+ requirement_completed: args.complete_requirement,
1023
+ pending_batch: pendingBatch,
1024
+ reused: reusedSync,
1025
+ idempotency_key: syncIdempotencyKey,
1026
+ development_warnings: boundedWarnings.page,
1027
+ development_warnings_total: boundedWarnings.total,
1028
+ development_warnings_truncated: boundedWarnings.truncated,
327
1029
  }),
328
1030
  },
329
1031
  ],
330
1032
  };
331
1033
  }
332
1034
  export async function handleGetPendingChanges(rawArgs, context) {
333
- const { getActiveRequirementStmt, listPendingChangesStmt } = context.getStatements();
1035
+ const { listPendingChangesStmt } = context.getStatements();
334
1036
  const args = GetPendingChangesArgsSchema.parse(rawArgs);
335
1037
  flushPendingChangeBuffer();
336
1038
  const offset = args.offset;
@@ -340,7 +1042,8 @@ export async function handleGetPendingChanges(rawArgs, context) {
340
1042
  const total = mergedPending.total;
341
1043
  const truncated = mergedPending.truncated;
342
1044
  const pending = mergedPending.page;
343
- const activeForScope = getActiveRequirementStmt.get();
1045
+ const resolution = resolveActiveRequirement(context, args);
1046
+ const activeForScope = resolution.requirement;
344
1047
  const development_warnings = [
345
1048
  ...buildDevelopmentWarnings(pending),
346
1049
  ...(activeForScope ? buildScopeDriftWarnings({ requirement: activeForScope, files: pending }) : []),
@@ -357,41 +1060,74 @@ export async function handleGetPendingChanges(rawArgs, context) {
357
1060
  content: [
358
1061
  {
359
1062
  type: "text",
360
- text: toolJson({ ok: true, total, offset, limit, truncated, pending, development_warnings }),
1063
+ text: toolJson({
1064
+ ok: true,
1065
+ total,
1066
+ offset,
1067
+ limit,
1068
+ truncated,
1069
+ pending,
1070
+ requirement_resolution: {
1071
+ requirement_id: activeForScope?.id ?? null,
1072
+ ambiguous: resolution.ambiguous,
1073
+ active_count: resolution.active_count,
1074
+ },
1075
+ development_warnings,
1076
+ }),
361
1077
  },
362
1078
  ],
363
1079
  };
364
1080
  }
365
1081
  export async function handleCompleteRequirement(rawArgs, context) {
366
- const db = context.getDb();
367
- const { completeAllActiveRequirementsStmt, getActiveRequirementStmt, completeRequirementByIdStmt } = context.getStatements();
368
1082
  const args = CompleteRequirementArgsSchema.parse(rawArgs);
369
1083
  flushPendingChangeBuffer();
370
1084
  const updated = [];
371
1085
  if (args.all_active) {
372
- const activeRows = db.prepare(`SELECT id FROM requirements WHERE status = 'active' ORDER BY created_at DESC, id DESC`).all().slice(0, 200);
373
1086
  try {
374
- completeAllActiveRequirementsStmt.run();
375
- completeAllActiveRequirementMemoryItems();
1087
+ const completedIds = completeAllActiveRequirementMemoryItems();
1088
+ for (const id of completedIds)
1089
+ updated.push({ id });
376
1090
  }
377
1091
  catch (err) {
378
- console.error("[vectormind] complete all active requirements failed:", err);
1092
+ return {
1093
+ isError: true,
1094
+ content: [{ type: "text", text: toolJson({ ok: false, error: `Failed to complete active requirements atomically: ${String(err)}` }) }],
1095
+ };
379
1096
  }
380
- for (const r of activeRows)
381
- updated.push({ id: r.id });
382
1097
  logActivity("complete_requirement", { all_active: true, completed: updated.map((u) => u.id) });
383
1098
  return { content: [{ type: "text", text: toolJson({ ok: true, completed: updated }) }] };
384
1099
  }
385
- const targetId = args.req_id ?? getActiveRequirementStmt.get()?.id ?? null;
1100
+ const resolution = resolveActiveRequirement(context, args);
1101
+ if (resolution.ambiguous) {
1102
+ return {
1103
+ isError: true,
1104
+ content: [{
1105
+ type: "text",
1106
+ text: toolJson({
1107
+ ok: false,
1108
+ error: "Multiple active requirements exist. Pass req_id or goal_key to complete_requirement.",
1109
+ active_count: resolution.active_count,
1110
+ }),
1111
+ }],
1112
+ };
1113
+ }
1114
+ const targetId = resolution.requirement?.id ?? null;
386
1115
  if (!targetId) {
387
1116
  return { content: [{ type: "text", text: toolJson({ ok: true, completed: [] }) }] };
388
1117
  }
389
1118
  try {
390
- completeRequirementByIdStmt.run(targetId);
391
- completeRequirementMemoryItemsByReqId(targetId);
1119
+ if (!completeRequirementMemoryItemsByReqId(targetId)) {
1120
+ return {
1121
+ isError: true,
1122
+ content: [{ type: "text", text: toolJson({ ok: false, error: `Requirement ${targetId} is no longer active.` }) }],
1123
+ };
1124
+ }
392
1125
  }
393
1126
  catch (err) {
394
- console.error("[vectormind] complete requirement failed:", err);
1127
+ return {
1128
+ isError: true,
1129
+ content: [{ type: "text", text: toolJson({ ok: false, error: `Failed to complete requirement atomically: ${String(err)}` }) }],
1130
+ };
395
1131
  }
396
1132
  logActivity("complete_requirement", { req_id: targetId });
397
1133
  return { content: [{ type: "text", text: toolJson({ ok: true, completed: [{ id: targetId }] }) }] };