@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.
- package/README.md +99 -72
- package/admin-panel/README.md +77 -0
- package/admin-panel/dist/client/assets/index-Bzyjdhh2.css +1 -0
- package/admin-panel/dist/client/assets/index-Ph73ewzt.js +9 -0
- package/admin-panel/dist/client/index.html +14 -0
- package/admin-panel/package.json +25 -0
- package/admin-panel/server/index.mjs +1424 -0
- package/admin-panel/server/security.mjs +117 -0
- package/admin-panel/server/security.test.mjs +113 -0
- package/admin-panel/server/storage.mjs +49 -0
- package/admin-panel/server/storage.test.mjs +36 -0
- package/dist/builtin-conventions.js +1 -1
- package/dist/builtin-conventions.js.map +1 -1
- package/dist/builtin-instructions.d.ts +1 -1
- package/dist/builtin-instructions.js +1 -1
- package/dist/builtin-instructions.js.map +1 -1
- package/dist/checkpoint-snapshot.d.ts +56 -0
- package/dist/checkpoint-snapshot.js +411 -0
- package/dist/checkpoint-snapshot.js.map +1 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +25 -1
- package/dist/config.js.map +1 -1
- package/dist/context-governance.d.ts +56 -0
- package/dist/context-governance.js +176 -0
- package/dist/context-governance.js.map +1 -0
- package/dist/database-runtime.d.ts +6 -1
- package/dist/database-runtime.js +555 -127
- package/dist/database-runtime.js.map +1 -1
- package/dist/development-warnings.d.ts +2 -1
- package/dist/development-warnings.js +92 -22
- package/dist/development-warnings.js.map +1 -1
- package/dist/file-indexing.d.ts +2 -1
- package/dist/file-indexing.js +127 -47
- package/dist/file-indexing.js.map +1 -1
- package/dist/grep.js +4 -0
- package/dist/grep.js.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/large-file-split.d.ts +53 -8
- package/dist/large-file-split.js +294 -75
- package/dist/large-file-split.js.map +1 -1
- package/dist/memory-maintenance/compaction.d.ts +7 -0
- package/dist/memory-maintenance/compaction.js +133 -0
- package/dist/memory-maintenance/compaction.js.map +1 -0
- package/dist/memory-maintenance/context.d.ts +37 -0
- package/dist/memory-maintenance/context.js +52 -0
- package/dist/memory-maintenance/context.js.map +1 -0
- package/dist/memory-maintenance/index-prune.d.ts +22 -0
- package/dist/memory-maintenance/index-prune.js +243 -0
- package/dist/memory-maintenance/index-prune.js.map +1 -0
- package/dist/memory-maintenance/metrics.d.ts +5 -0
- package/dist/memory-maintenance/metrics.js +30 -0
- package/dist/memory-maintenance/metrics.js.map +1 -0
- package/dist/memory-maintenance/pending.d.ts +4 -0
- package/dist/memory-maintenance/pending.js +74 -0
- package/dist/memory-maintenance/pending.js.map +1 -0
- package/dist/memory-maintenance/purge.d.ts +7 -0
- package/dist/memory-maintenance/purge.js +115 -0
- package/dist/memory-maintenance/purge.js.map +1 -0
- package/dist/memory-maintenance/runner.d.ts +5 -0
- package/dist/memory-maintenance/runner.js +182 -0
- package/dist/memory-maintenance/runner.js.map +1 -0
- package/dist/memory-maintenance/sqlite.d.ts +10 -0
- package/dist/memory-maintenance/sqlite.js +67 -0
- package/dist/memory-maintenance/sqlite.js.map +1 -0
- package/dist/memory-maintenance/types.d.ts +87 -0
- package/dist/memory-maintenance/types.js +2 -0
- package/dist/memory-maintenance/types.js.map +1 -0
- package/dist/memory-maintenance.d.ts +5 -80
- package/dist/memory-maintenance.js +3 -536
- package/dist/memory-maintenance.js.map +1 -1
- package/dist/memory-mutations.d.ts +3 -3
- package/dist/memory-mutations.js +109 -43
- package/dist/memory-mutations.js.map +1 -1
- package/dist/memory-recall.d.ts +3 -1
- package/dist/memory-recall.js +33 -1
- package/dist/memory-recall.js.map +1 -1
- package/dist/operation-scope.js +42 -12
- package/dist/operation-scope.js.map +1 -1
- package/dist/path-containment.d.ts +5 -0
- package/dist/path-containment.js +64 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-filters.d.ts +1 -1
- package/dist/path-filters.js +7 -6
- package/dist/path-filters.js.map +1 -1
- package/dist/path-rules.d.ts +1 -0
- package/dist/path-rules.js +23 -1
- package/dist/path-rules.js.map +1 -1
- package/dist/pending-changes.d.ts +5 -0
- package/dist/pending-changes.js +30 -16
- package/dist/pending-changes.js.map +1 -1
- package/dist/project-files.d.ts +1 -0
- package/dist/project-files.js +19 -10
- package/dist/project-files.js.map +1 -1
- package/dist/root.d.ts +4 -1
- package/dist/root.js +34 -4
- package/dist/root.js.map +1 -1
- package/dist/rtk-integrity.d.ts +5 -0
- package/dist/rtk-integrity.js +35 -0
- package/dist/rtk-integrity.js.map +1 -0
- package/dist/rtk-shim.js +22 -9
- package/dist/rtk-shim.js.map +1 -1
- package/dist/rtk-tools.d.ts +8 -0
- package/dist/rtk-tools.js +59 -42
- package/dist/rtk-tools.js.map +1 -1
- package/dist/server-instructions.js +8 -88
- package/dist/server-instructions.js.map +1 -1
- package/dist/tool-catalog.d.ts +2 -0
- package/dist/tool-catalog.js +77 -24
- package/dist/tool-catalog.js.map +1 -1
- package/dist/tool-handlers/context-recovery.js +79 -54
- package/dist/tool-handlers/context-recovery.js.map +1 -1
- package/dist/tool-handlers/large-files.js +615 -41
- package/dist/tool-handlers/large-files.js.map +1 -1
- package/dist/tool-handlers/memory-diagnostics.js +240 -45
- package/dist/tool-handlers/memory-diagnostics.js.map +1 -1
- package/dist/tool-handlers/memory.js +167 -22
- package/dist/tool-handlers/memory.js.map +1 -1
- package/dist/tool-handlers/operations.js +28 -8
- package/dist/tool-handlers/operations.js.map +1 -1
- package/dist/tool-handlers/requirement-status.d.ts +5 -0
- package/dist/tool-handlers/requirement-status.js +267 -0
- package/dist/tool-handlers/requirement-status.js.map +1 -0
- package/dist/tool-handlers/requirements.js +851 -115
- package/dist/tool-handlers/requirements.js.map +1 -1
- package/dist/tool-handlers.d.ts +4 -0
- package/dist/tool-handlers.js +439 -6
- package/dist/tool-handlers.js.map +1 -1
- package/dist/tool-output.d.ts +71 -2
- package/dist/tool-output.js +56 -13
- package/dist/tool-output.js.map +1 -1
- package/dist/tool-schemas.d.ts +243 -0
- package/dist/tool-schemas.js +112 -26
- package/dist/tool-schemas.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/docs/capability-matrix.md +23 -17
- package/package.json +21 -5
- package/skills/vector-mind-autopilot/SKILL.md +82 -23
- package/skills/vector-mind-autopilot/references/claude-project-instructions.md +14 -11
- 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,
|
|
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,
|
|
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:
|
|
235
|
+
close_previous: shouldClosePrevious,
|
|
30
236
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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:
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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:
|
|
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
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
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 =
|
|
136
|
-
? "
|
|
137
|
-
:
|
|
138
|
-
? "
|
|
139
|
-
:
|
|
140
|
-
? "
|
|
141
|
-
: hasHugeFile
|
|
142
|
-
? "
|
|
143
|
-
:
|
|
144
|
-
? "
|
|
145
|
-
:
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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,
|
|
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
|
|
198
|
-
const active =
|
|
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
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
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:
|
|
234
|
-
dbFilePath:
|
|
814
|
+
rawFile: contained.absolute,
|
|
815
|
+
dbFilePath: contained.dbFilePath,
|
|
235
816
|
event: p.last_event,
|
|
236
|
-
source:
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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:
|
|
266
|
-
event:
|
|
267
|
-
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 (!
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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({
|
|
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
|
-
|
|
375
|
-
|
|
1087
|
+
const completedIds = completeAllActiveRequirementMemoryItems();
|
|
1088
|
+
for (const id of completedIds)
|
|
1089
|
+
updated.push({ id });
|
|
376
1090
|
}
|
|
377
1091
|
catch (err) {
|
|
378
|
-
|
|
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
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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 }] }) }] };
|