@coreyuan/vector-mind 1.0.54 → 1.1.1

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 (117) hide show
  1. package/README.md +107 -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 +6 -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 +1 -1
  21. package/dist/config.js +1 -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 +3 -1
  27. package/dist/database-runtime.js +232 -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 +90 -20
  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 +5 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/large-file-split.d.ts +52 -8
  40. package/dist/large-file-split.js +290 -76
  41. package/dist/large-file-split.js.map +1 -1
  42. package/dist/memory-maintenance/compaction.js +3 -2
  43. package/dist/memory-maintenance/compaction.js.map +1 -1
  44. package/dist/memory-maintenance/metrics.d.ts +1 -1
  45. package/dist/memory-maintenance/metrics.js +11 -3
  46. package/dist/memory-maintenance/metrics.js.map +1 -1
  47. package/dist/memory-maintenance/purge.js +1 -1
  48. package/dist/memory-maintenance/purge.js.map +1 -1
  49. package/dist/memory-maintenance/runner.d.ts +1 -0
  50. package/dist/memory-maintenance/runner.js +15 -3
  51. package/dist/memory-maintenance/runner.js.map +1 -1
  52. package/dist/memory-maintenance/types.d.ts +1 -0
  53. package/dist/memory-mutations.d.ts +3 -3
  54. package/dist/memory-mutations.js +109 -43
  55. package/dist/memory-mutations.js.map +1 -1
  56. package/dist/memory-recall.js +16 -1
  57. package/dist/memory-recall.js.map +1 -1
  58. package/dist/operation-scope.js +42 -12
  59. package/dist/operation-scope.js.map +1 -1
  60. package/dist/path-containment.d.ts +5 -0
  61. package/dist/path-containment.js +64 -0
  62. package/dist/path-containment.js.map +1 -0
  63. package/dist/path-filters.d.ts +1 -1
  64. package/dist/path-filters.js +7 -6
  65. package/dist/path-filters.js.map +1 -1
  66. package/dist/pending-changes.d.ts +5 -0
  67. package/dist/pending-changes.js +26 -16
  68. package/dist/pending-changes.js.map +1 -1
  69. package/dist/project-files.d.ts +1 -0
  70. package/dist/project-files.js +19 -10
  71. package/dist/project-files.js.map +1 -1
  72. package/dist/root.d.ts +4 -1
  73. package/dist/root.js +34 -4
  74. package/dist/root.js.map +1 -1
  75. package/dist/rtk-integrity.d.ts +5 -0
  76. package/dist/rtk-integrity.js +35 -0
  77. package/dist/rtk-integrity.js.map +1 -0
  78. package/dist/rtk-shim.js +22 -9
  79. package/dist/rtk-shim.js.map +1 -1
  80. package/dist/rtk-tools.d.ts +8 -0
  81. package/dist/rtk-tools.js +59 -42
  82. package/dist/rtk-tools.js.map +1 -1
  83. package/dist/server-instructions.js +9 -88
  84. package/dist/server-instructions.js.map +1 -1
  85. package/dist/tool-catalog.d.ts +2 -0
  86. package/dist/tool-catalog.js +76 -23
  87. package/dist/tool-catalog.js.map +1 -1
  88. package/dist/tool-handlers/context-recovery.js +79 -54
  89. package/dist/tool-handlers/context-recovery.js.map +1 -1
  90. package/dist/tool-handlers/large-files.js +600 -44
  91. package/dist/tool-handlers/large-files.js.map +1 -1
  92. package/dist/tool-handlers/memory-diagnostics.js +240 -45
  93. package/dist/tool-handlers/memory-diagnostics.js.map +1 -1
  94. package/dist/tool-handlers/memory.js +167 -22
  95. package/dist/tool-handlers/memory.js.map +1 -1
  96. package/dist/tool-handlers/operations.js +28 -8
  97. package/dist/tool-handlers/operations.js.map +1 -1
  98. package/dist/tool-handlers/requirement-status.d.ts +5 -0
  99. package/dist/tool-handlers/requirement-status.js +267 -0
  100. package/dist/tool-handlers/requirement-status.js.map +1 -0
  101. package/dist/tool-handlers/requirements.js +786 -97
  102. package/dist/tool-handlers/requirements.js.map +1 -1
  103. package/dist/tool-handlers.d.ts +4 -0
  104. package/dist/tool-handlers.js +183 -10
  105. package/dist/tool-handlers.js.map +1 -1
  106. package/dist/tool-output.d.ts +32 -1
  107. package/dist/tool-output.js +31 -12
  108. package/dist/tool-output.js.map +1 -1
  109. package/dist/tool-schemas.d.ts +235 -0
  110. package/dist/tool-schemas.js +98 -26
  111. package/dist/tool-schemas.js.map +1 -1
  112. package/dist/types.d.ts +1 -0
  113. package/docs/capability-matrix.md +24 -17
  114. package/package.json +19 -6
  115. package/skills/vector-mind-autopilot/SKILL.md +82 -23
  116. package/skills/vector-mind-autopilot/references/claude-project-instructions.md +14 -11
  117. 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, upsertSyncedFileStateStmt, 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,7 +740,35 @@ 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
  ],
@@ -212,32 +776,48 @@ export async function handleSyncChangeIntent(rawArgs, context) {
212
776
  }
213
777
  const created = [];
214
778
  let createdChange = null;
779
+ const reusedSync = false;
780
+ let pendingBatch = null;
215
781
  const fixPatternMemoryItem = { value: null };
216
782
  const synced_files = [];
783
+ let transactionReplay;
217
784
  const insertTx = db.transaction(() => {
785
+ transactionReplay = findExistingSync();
786
+ if (transactionReplay)
787
+ return;
788
+ const pendingPathsToDelete = new Set();
218
789
  const targets = [];
219
790
  if (explicitFiles.length) {
220
- for (const rawFile of explicitFiles) {
221
- const dbFilePath = normalizeToDbPath(rawFile);
222
- targets.push({ rawFile, dbFilePath, event: "manual", source: "args" });
223
- }
224
- for (const t of targets) {
225
- 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);
226
794
  }
227
795
  }
228
796
  else {
229
797
  const pendingAll = listPendingChangesStmt.all();
230
- 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
+ }
231
810
  if (merged.page.length) {
232
811
  for (const p of merged.page) {
812
+ const contained = toContainedFile(p.file_path, p.last_event === "unlink");
233
813
  targets.push({
234
- rawFile: p.file_path,
235
- dbFilePath: p.file_path,
814
+ rawFile: contained.absolute,
815
+ dbFilePath: contained.dbFilePath,
236
816
  event: p.last_event,
237
- source: p.source === "git" ? "pending" : "pending",
817
+ source: "pending",
238
818
  });
819
+ pendingPathsToDelete.add(p.file_path);
239
820
  }
240
- deleteAllPendingChangesStmt.run();
241
821
  }
242
822
  else {
243
823
  targets.push({
@@ -273,6 +853,33 @@ export async function handleSyncChangeIntent(rawArgs, context) {
273
853
  };
274
854
  });
275
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);
276
883
  const primaryFilePath = concreteFileStates.length === 1
277
884
  ? concreteFileStates[0].file_path
278
885
  : concreteFileStates.length > 1
@@ -298,6 +905,11 @@ export async function handleSyncChangeIntent(rawArgs, context) {
298
905
  file_count: fileCount,
299
906
  verification: args.verification ?? [],
300
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,
301
913
  });
302
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")}`));
303
915
  const memory_item_id = Number(memoryInfo.lastInsertRowid);
@@ -336,8 +948,23 @@ export async function handleSyncChangeIntent(rawArgs, context) {
336
948
  }), sha256Hex(content));
337
949
  fixPatternMemoryItem.value = { id: Number(memoryInfo.lastInsertRowid), kind: "fix_pattern", title };
338
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
+ }
339
956
  });
340
- 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);
341
968
  const development_warnings = [
342
969
  ...buildDevelopmentWarnings(synced_files, {
343
970
  includeUnspecified: synced_files.some((f) => f.file_path === "(unspecified)"),
@@ -348,6 +975,13 @@ export async function handleSyncChangeIntent(rawArgs, context) {
348
975
  files: synced_files,
349
976
  }),
350
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);
351
985
  logActivity("sync_change_intent", {
352
986
  req_id: active.id,
353
987
  title: active.title,
@@ -356,6 +990,10 @@ export async function handleSyncChangeIntent(rawArgs, context) {
356
990
  files_total: synced_files.length,
357
991
  fix_pattern_memory_id: fixPatternMemoryItem.value?.id ?? null,
358
992
  development_warnings: development_warnings.length,
993
+ large_file_split_deferrals: largeFileSplitDeferrals.length,
994
+ pending_batch: pendingBatch,
995
+ reused: reusedSync,
996
+ idempotency_key: syncIdempotencyKey,
359
997
  });
360
998
  return {
361
999
  content: [
@@ -364,20 +1002,37 @@ export async function handleSyncChangeIntent(rawArgs, context) {
364
1002
  text: toolJson({
365
1003
  ok: true,
366
1004
  linked_to_requirement: { id: active.id, title: active.title },
367
- synced_files,
368
- created,
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,
369
1011
  created_change: createdChange,
370
- verification: args.verification ?? [],
371
- verification_gaps: args.verification_gaps ?? [],
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,
372
1021
  created_fix_pattern: fixPatternMemoryItem.value,
373
- 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,
374
1029
  }),
375
1030
  },
376
1031
  ],
377
1032
  };
378
1033
  }
379
1034
  export async function handleGetPendingChanges(rawArgs, context) {
380
- const { getActiveRequirementStmt, listPendingChangesStmt } = context.getStatements();
1035
+ const { listPendingChangesStmt } = context.getStatements();
381
1036
  const args = GetPendingChangesArgsSchema.parse(rawArgs);
382
1037
  flushPendingChangeBuffer();
383
1038
  const offset = args.offset;
@@ -387,7 +1042,8 @@ export async function handleGetPendingChanges(rawArgs, context) {
387
1042
  const total = mergedPending.total;
388
1043
  const truncated = mergedPending.truncated;
389
1044
  const pending = mergedPending.page;
390
- const activeForScope = getActiveRequirementStmt.get();
1045
+ const resolution = resolveActiveRequirement(context, args);
1046
+ const activeForScope = resolution.requirement;
391
1047
  const development_warnings = [
392
1048
  ...buildDevelopmentWarnings(pending),
393
1049
  ...(activeForScope ? buildScopeDriftWarnings({ requirement: activeForScope, files: pending }) : []),
@@ -404,41 +1060,74 @@ export async function handleGetPendingChanges(rawArgs, context) {
404
1060
  content: [
405
1061
  {
406
1062
  type: "text",
407
- 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
+ }),
408
1077
  },
409
1078
  ],
410
1079
  };
411
1080
  }
412
1081
  export async function handleCompleteRequirement(rawArgs, context) {
413
- const db = context.getDb();
414
- const { completeAllActiveRequirementsStmt, getActiveRequirementStmt, completeRequirementByIdStmt } = context.getStatements();
415
1082
  const args = CompleteRequirementArgsSchema.parse(rawArgs);
416
1083
  flushPendingChangeBuffer();
417
1084
  const updated = [];
418
1085
  if (args.all_active) {
419
- const activeRows = db.prepare(`SELECT id FROM requirements WHERE status = 'active' ORDER BY created_at DESC, id DESC`).all().slice(0, 200);
420
1086
  try {
421
- completeAllActiveRequirementsStmt.run();
422
- completeAllActiveRequirementMemoryItems();
1087
+ const completedIds = completeAllActiveRequirementMemoryItems();
1088
+ for (const id of completedIds)
1089
+ updated.push({ id });
423
1090
  }
424
1091
  catch (err) {
425
- 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
+ };
426
1096
  }
427
- for (const r of activeRows)
428
- updated.push({ id: r.id });
429
1097
  logActivity("complete_requirement", { all_active: true, completed: updated.map((u) => u.id) });
430
1098
  return { content: [{ type: "text", text: toolJson({ ok: true, completed: updated }) }] };
431
1099
  }
432
- 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;
433
1115
  if (!targetId) {
434
1116
  return { content: [{ type: "text", text: toolJson({ ok: true, completed: [] }) }] };
435
1117
  }
436
1118
  try {
437
- completeRequirementByIdStmt.run(targetId);
438
- 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
+ }
439
1125
  }
440
1126
  catch (err) {
441
- 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
+ };
442
1131
  }
443
1132
  logActivity("complete_requirement", { req_id: targetId });
444
1133
  return { content: [{ type: "text", text: toolJson({ ok: true, completed: [{ id: targetId }] }) }] };