gsd-pi 2.59.0-dev.023bd39 → 2.59.0-dev.d77b3dd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/gsd/auto/phases.js +54 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +8 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +40 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +70 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +51 -5
- package/dist/resources/extensions/gsd/captures.js +54 -1
- package/dist/resources/extensions/gsd/complexity-classifier.js +1 -1
- package/dist/resources/extensions/gsd/context-masker.js +68 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +7 -0
- package/dist/resources/extensions/gsd/gsd-db.js +2 -2
- package/dist/resources/extensions/gsd/model-router.js +123 -4
- package/dist/resources/extensions/gsd/phase-anchor.js +56 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +46 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/rethink.md +7 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +6 -1
- package/dist/resources/extensions/gsd/rethink.js +5 -2
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/status-guards.js +4 -3
- package/dist/resources/extensions/gsd/triage-resolution.js +128 -1
- package/dist/resources/extensions/gsd/triage-ui.js +12 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +60 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +12 -3
- package/src/resources/extensions/gsd/auto-post-unit.ts +48 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +78 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +53 -4
- package/src/resources/extensions/gsd/captures.ts +71 -2
- package/src/resources/extensions/gsd/complexity-classifier.ts +1 -1
- package/src/resources/extensions/gsd/context-masker.ts +74 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +7 -0
- package/src/resources/extensions/gsd/gsd-db.ts +2 -2
- package/src/resources/extensions/gsd/model-router.ts +171 -8
- package/src/resources/extensions/gsd/phase-anchor.ts +71 -0
- package/src/resources/extensions/gsd/preferences-types.ts +9 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +38 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/rethink.md +7 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +6 -1
- package/src/resources/extensions/gsd/rethink.ts +5 -2
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/status-guards.ts +4 -3
- package/src/resources/extensions/gsd/tests/context-masker.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +87 -1
- package/src/resources/extensions/gsd/tests/phase-anchor.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/stop-backtrack.test.ts +216 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +1 -1
- package/src/resources/extensions/gsd/triage-resolution.ts +144 -1
- package/src/resources/extensions/gsd/triage-ui.ts +12 -3
- /package/dist/web/standalone/.next/static/{QlWL-8CXgQpzV3ehkNMzh → t_cBZAENjaOJIRST3dw08}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{QlWL-8CXgQpzV3ehkNMzh → t_cBZAENjaOJIRST3dw08}/_ssgManifest.js +0 -0
|
@@ -76,6 +76,65 @@ const MODEL_COST_PER_1K_INPUT = {
|
|
|
76
76
|
"gemini-2.5-pro": 0.00125,
|
|
77
77
|
"deepseek-chat": 0.00014,
|
|
78
78
|
};
|
|
79
|
+
export const MODEL_CAPABILITY_PROFILES = {
|
|
80
|
+
"claude-opus-4-6": { coding: 95, debugging: 90, research: 85, reasoning: 95, speed: 30, longContext: 80, instruction: 90 },
|
|
81
|
+
"claude-sonnet-4-6": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
|
|
82
|
+
"claude-haiku-4-5": { coding: 60, debugging: 50, research: 45, reasoning: 50, speed: 95, longContext: 50, instruction: 75 },
|
|
83
|
+
"gpt-4o": { coding: 80, debugging: 75, research: 70, reasoning: 75, speed: 65, longContext: 70, instruction: 80 },
|
|
84
|
+
"gpt-4o-mini": { coding: 55, debugging: 45, research: 40, reasoning: 45, speed: 90, longContext: 45, instruction: 70 },
|
|
85
|
+
"gemini-2.5-pro": { coding: 75, debugging: 70, research: 85, reasoning: 75, speed: 55, longContext: 90, instruction: 75 },
|
|
86
|
+
"gemini-2.0-flash": { coding: 50, debugging: 40, research: 50, reasoning: 40, speed: 95, longContext: 60, instruction: 65 },
|
|
87
|
+
"deepseek-chat": { coding: 75, debugging: 65, research: 55, reasoning: 70, speed: 70, longContext: 55, instruction: 65 },
|
|
88
|
+
"o3": { coding: 80, debugging: 85, research: 80, reasoning: 92, speed: 25, longContext: 70, instruction: 85 },
|
|
89
|
+
};
|
|
90
|
+
const BASE_REQUIREMENTS = {
|
|
91
|
+
"execute-task": { coding: 0.9, instruction: 0.7, speed: 0.3 },
|
|
92
|
+
"research-milestone": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
|
|
93
|
+
"research-slice": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
|
|
94
|
+
"plan-milestone": { reasoning: 0.9, coding: 0.5 },
|
|
95
|
+
"plan-slice": { reasoning: 0.9, coding: 0.5 },
|
|
96
|
+
"replan-slice": { reasoning: 0.9, debugging: 0.6, coding: 0.5 },
|
|
97
|
+
"reassess-roadmap": { reasoning: 0.9, research: 0.5 },
|
|
98
|
+
"complete-slice": { instruction: 0.8, speed: 0.7 },
|
|
99
|
+
"run-uat": { instruction: 0.7, speed: 0.8 },
|
|
100
|
+
"discuss-milestone": { reasoning: 0.6, instruction: 0.7 },
|
|
101
|
+
"complete-milestone": { instruction: 0.8, reasoning: 0.5 },
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Compute a task requirement vector from unit type and optional metadata.
|
|
105
|
+
*/
|
|
106
|
+
export function computeTaskRequirements(unitType, metadata) {
|
|
107
|
+
const base = { ...(BASE_REQUIREMENTS[unitType] ?? { reasoning: 0.5 }) };
|
|
108
|
+
if (unitType === "execute-task" && metadata) {
|
|
109
|
+
if (metadata.tags?.some(t => /^(docs?|readme|comment|config|typo|rename)$/i.test(t))) {
|
|
110
|
+
return { ...base, instruction: 0.9, coding: 0.3, speed: 0.7 };
|
|
111
|
+
}
|
|
112
|
+
if (metadata.complexityKeywords?.some(k => k === "concurrency" || k === "compatibility")) {
|
|
113
|
+
return { ...base, debugging: 0.9, reasoning: 0.8 };
|
|
114
|
+
}
|
|
115
|
+
if (metadata.complexityKeywords?.some(k => k === "migration" || k === "architecture")) {
|
|
116
|
+
return { ...base, reasoning: 0.9, coding: 0.8 };
|
|
117
|
+
}
|
|
118
|
+
if ((metadata.fileCount ?? 0) >= 6 || (metadata.estimatedLines ?? 0) >= 500) {
|
|
119
|
+
return { ...base, coding: 0.9, reasoning: 0.7 };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return base;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Score a model against a task requirement vector.
|
|
126
|
+
* Returns weighted average in range 0–100. Returns 50 for empty requirements.
|
|
127
|
+
*/
|
|
128
|
+
export function scoreModel(capabilities, requirements) {
|
|
129
|
+
let weightedSum = 0;
|
|
130
|
+
let weightSum = 0;
|
|
131
|
+
for (const [dim, weight] of Object.entries(requirements)) {
|
|
132
|
+
const capability = capabilities[dim] ?? 50;
|
|
133
|
+
weightedSum += weight * capability;
|
|
134
|
+
weightSum += weight;
|
|
135
|
+
}
|
|
136
|
+
return weightSum > 0 ? weightedSum / weightSum : 50;
|
|
137
|
+
}
|
|
79
138
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
80
139
|
/**
|
|
81
140
|
* Resolve the model to use for a given complexity tier.
|
|
@@ -88,7 +147,7 @@ const MODEL_COST_PER_1K_INPUT = {
|
|
|
88
147
|
* @param routingConfig Dynamic routing configuration
|
|
89
148
|
* @param availableModelIds List of available model IDs (from registry)
|
|
90
149
|
*/
|
|
91
|
-
export function resolveModelForComplexity(classification, phaseConfig, routingConfig, availableModelIds) {
|
|
150
|
+
export function resolveModelForComplexity(classification, phaseConfig, routingConfig, availableModelIds, unitType, metadata) {
|
|
92
151
|
// If no phase config or routing disabled, pass through
|
|
93
152
|
if (!phaseConfig || !routingConfig.enabled) {
|
|
94
153
|
return {
|
|
@@ -127,18 +186,31 @@ export function resolveModelForComplexity(classification, phaseConfig, routingCo
|
|
|
127
186
|
};
|
|
128
187
|
}
|
|
129
188
|
// Find the best model for the requested tier
|
|
130
|
-
const
|
|
189
|
+
const useCapabilityScoring = routingConfig.capability_routing && unitType;
|
|
190
|
+
let targetModelId;
|
|
191
|
+
let capabilityScores;
|
|
192
|
+
let taskRequirements;
|
|
193
|
+
let selectionMethod = "tier-only";
|
|
194
|
+
if (useCapabilityScoring) {
|
|
195
|
+
const result = findModelForTierWithCapability(requestedTier, routingConfig, availableModelIds, routingConfig.cross_provider !== false, unitType, metadata);
|
|
196
|
+
targetModelId = result.modelId;
|
|
197
|
+
capabilityScores = Object.keys(result.scores).length > 0 ? result.scores : undefined;
|
|
198
|
+
taskRequirements = Object.keys(result.requirements).length > 0 ? result.requirements : undefined;
|
|
199
|
+
selectionMethod = capabilityScores ? "capability-scored" : "tier-only";
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
targetModelId = findModelForTier(requestedTier, routingConfig, availableModelIds, routingConfig.cross_provider !== false);
|
|
203
|
+
}
|
|
131
204
|
if (!targetModelId) {
|
|
132
|
-
// No suitable model found — use configured primary
|
|
133
205
|
return {
|
|
134
206
|
modelId: configuredPrimary,
|
|
135
207
|
fallbacks: phaseConfig.fallbacks,
|
|
136
208
|
tier: requestedTier,
|
|
137
209
|
wasDowngraded: false,
|
|
138
210
|
reason: `no ${requestedTier}-tier model available`,
|
|
211
|
+
selectionMethod,
|
|
139
212
|
};
|
|
140
213
|
}
|
|
141
|
-
// Build fallback chain: [downgraded_model, ...configured_fallbacks, configured_primary]
|
|
142
214
|
const fallbacks = [
|
|
143
215
|
...phaseConfig.fallbacks.filter(f => f !== targetModelId),
|
|
144
216
|
configuredPrimary,
|
|
@@ -149,6 +221,9 @@ export function resolveModelForComplexity(classification, phaseConfig, routingCo
|
|
|
149
221
|
tier: requestedTier,
|
|
150
222
|
wasDowngraded: true,
|
|
151
223
|
reason: classification.reason,
|
|
224
|
+
selectionMethod,
|
|
225
|
+
capabilityScores,
|
|
226
|
+
taskRequirements,
|
|
152
227
|
};
|
|
153
228
|
}
|
|
154
229
|
/**
|
|
@@ -168,6 +243,7 @@ export function escalateTier(currentTier) {
|
|
|
168
243
|
export function defaultRoutingConfig() {
|
|
169
244
|
return {
|
|
170
245
|
enabled: true,
|
|
246
|
+
capability_routing: false,
|
|
171
247
|
escalate_on_failure: true,
|
|
172
248
|
budget_pressure: true,
|
|
173
249
|
cross_provider: true,
|
|
@@ -231,6 +307,49 @@ function findModelForTier(tier, config, availableModelIds, crossProvider) {
|
|
|
231
307
|
});
|
|
232
308
|
return candidates[0] ?? null;
|
|
233
309
|
}
|
|
310
|
+
function findModelForTierWithCapability(tier, config, availableModelIds, crossProvider, unitType, metadata) {
|
|
311
|
+
const explicitModel = config.tier_models?.[tier];
|
|
312
|
+
if (explicitModel) {
|
|
313
|
+
const match = availableModelIds.find(id => {
|
|
314
|
+
const bareAvail = id.includes("/") ? id.split("/").pop() : id;
|
|
315
|
+
const bareExplicit = explicitModel.includes("/") ? explicitModel.split("/").pop() : explicitModel;
|
|
316
|
+
return bareAvail === bareExplicit || id === explicitModel;
|
|
317
|
+
});
|
|
318
|
+
if (match)
|
|
319
|
+
return { modelId: match, scores: {}, requirements: {} };
|
|
320
|
+
}
|
|
321
|
+
const requirements = computeTaskRequirements(unitType, metadata);
|
|
322
|
+
const candidates = availableModelIds.filter(id => getModelTier(id) === tier);
|
|
323
|
+
if (candidates.length === 0)
|
|
324
|
+
return { modelId: null, scores: {}, requirements };
|
|
325
|
+
const scores = {};
|
|
326
|
+
for (const id of candidates) {
|
|
327
|
+
const bareId = id.includes("/") ? id.split("/").pop() : id;
|
|
328
|
+
const profile = getModelProfile(bareId);
|
|
329
|
+
scores[id] = scoreModel(profile, requirements);
|
|
330
|
+
}
|
|
331
|
+
candidates.sort((a, b) => {
|
|
332
|
+
const scoreDiff = scores[b] - scores[a];
|
|
333
|
+
if (Math.abs(scoreDiff) > 2)
|
|
334
|
+
return scoreDiff;
|
|
335
|
+
if (crossProvider) {
|
|
336
|
+
const costDiff = getModelCost(a) - getModelCost(b);
|
|
337
|
+
if (costDiff !== 0)
|
|
338
|
+
return costDiff;
|
|
339
|
+
}
|
|
340
|
+
return a.localeCompare(b);
|
|
341
|
+
});
|
|
342
|
+
return { modelId: candidates[0], scores, requirements };
|
|
343
|
+
}
|
|
344
|
+
function getModelProfile(bareId) {
|
|
345
|
+
if (MODEL_CAPABILITY_PROFILES[bareId])
|
|
346
|
+
return MODEL_CAPABILITY_PROFILES[bareId];
|
|
347
|
+
for (const [knownId, profile] of Object.entries(MODEL_CAPABILITY_PROFILES)) {
|
|
348
|
+
if (bareId.includes(knownId) || knownId.includes(bareId))
|
|
349
|
+
return profile;
|
|
350
|
+
}
|
|
351
|
+
return { coding: 50, debugging: 50, research: 50, reasoning: 50, speed: 50, longContext: 50, instruction: 50 };
|
|
352
|
+
}
|
|
234
353
|
function getModelCost(modelId) {
|
|
235
354
|
const bareId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
|
|
236
355
|
if (MODEL_COST_PER_1K_INPUT[bareId] !== undefined) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase handoff anchors — compact structured summaries written between
|
|
3
|
+
* GSD auto-mode phases so downstream agents inherit decisions, blockers,
|
|
4
|
+
* and intent without re-inferring from scratch.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { gsdRoot } from "./paths.js";
|
|
9
|
+
function anchorsDir(basePath, milestoneId) {
|
|
10
|
+
return join(gsdRoot(basePath), "milestones", milestoneId, "anchors");
|
|
11
|
+
}
|
|
12
|
+
function anchorPath(basePath, milestoneId, phase) {
|
|
13
|
+
return join(anchorsDir(basePath, milestoneId), `${phase}.json`);
|
|
14
|
+
}
|
|
15
|
+
export function writePhaseAnchor(basePath, milestoneId, anchor) {
|
|
16
|
+
const dir = anchorsDir(basePath, milestoneId);
|
|
17
|
+
if (!existsSync(dir)) {
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
writeFileSync(anchorPath(basePath, milestoneId, anchor.phase), JSON.stringify(anchor, null, 2), "utf-8");
|
|
21
|
+
}
|
|
22
|
+
export function readPhaseAnchor(basePath, milestoneId, phase) {
|
|
23
|
+
const path = anchorPath(basePath, milestoneId, phase);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function formatAnchorForPrompt(anchor) {
|
|
34
|
+
const lines = [
|
|
35
|
+
`## Handoff from ${anchor.phase}`,
|
|
36
|
+
"",
|
|
37
|
+
`**Intent:** ${anchor.intent}`,
|
|
38
|
+
];
|
|
39
|
+
if (anchor.decisions.length > 0) {
|
|
40
|
+
lines.push("", "**Decisions:**");
|
|
41
|
+
for (const d of anchor.decisions)
|
|
42
|
+
lines.push(`- ${d}`);
|
|
43
|
+
}
|
|
44
|
+
if (anchor.blockers.length > 0) {
|
|
45
|
+
lines.push("", "**Blockers:**");
|
|
46
|
+
for (const b of anchor.blockers)
|
|
47
|
+
lines.push(`- ${b}`);
|
|
48
|
+
}
|
|
49
|
+
if (anchor.nextSteps.length > 0) {
|
|
50
|
+
lines.push("", "**Next steps:**");
|
|
51
|
+
for (const s of anchor.nextSteps)
|
|
52
|
+
lines.push(`- ${s}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push("", "---");
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
@@ -425,6 +425,12 @@ export function validatePreferences(preferences) {
|
|
|
425
425
|
else
|
|
426
426
|
errors.push("dynamic_routing.hooks must be a boolean");
|
|
427
427
|
}
|
|
428
|
+
if (dr.capability_routing !== undefined) {
|
|
429
|
+
if (typeof dr.capability_routing === "boolean")
|
|
430
|
+
validDr.capability_routing = dr.capability_routing;
|
|
431
|
+
else
|
|
432
|
+
errors.push("dynamic_routing.capability_routing must be a boolean");
|
|
433
|
+
}
|
|
428
434
|
if (dr.tier_models !== undefined) {
|
|
429
435
|
if (typeof dr.tier_models === "object" && dr.tier_models !== null) {
|
|
430
436
|
const tm = dr.tier_models;
|
|
@@ -452,6 +458,46 @@ export function validatePreferences(preferences) {
|
|
|
452
458
|
errors.push("dynamic_routing must be an object");
|
|
453
459
|
}
|
|
454
460
|
}
|
|
461
|
+
// ─── Context Management ──────────────────────────────────────────────
|
|
462
|
+
if (preferences.context_management !== undefined) {
|
|
463
|
+
if (typeof preferences.context_management === "object" && preferences.context_management !== null) {
|
|
464
|
+
const cm = preferences.context_management;
|
|
465
|
+
const validCm = {};
|
|
466
|
+
if (cm.observation_masking !== undefined) {
|
|
467
|
+
if (typeof cm.observation_masking === "boolean")
|
|
468
|
+
validCm.observation_masking = cm.observation_masking;
|
|
469
|
+
else
|
|
470
|
+
errors.push("context_management.observation_masking must be a boolean");
|
|
471
|
+
}
|
|
472
|
+
if (cm.observation_mask_turns !== undefined) {
|
|
473
|
+
const turns = cm.observation_mask_turns;
|
|
474
|
+
if (typeof turns === "number" && turns >= 1 && turns <= 50)
|
|
475
|
+
validCm.observation_mask_turns = turns;
|
|
476
|
+
else
|
|
477
|
+
errors.push("context_management.observation_mask_turns must be a number between 1 and 50");
|
|
478
|
+
}
|
|
479
|
+
if (cm.compaction_threshold_percent !== undefined) {
|
|
480
|
+
const pct = cm.compaction_threshold_percent;
|
|
481
|
+
if (typeof pct === "number" && pct >= 0.5 && pct <= 0.95)
|
|
482
|
+
validCm.compaction_threshold_percent = pct;
|
|
483
|
+
else
|
|
484
|
+
errors.push("context_management.compaction_threshold_percent must be a number between 0.5 and 0.95");
|
|
485
|
+
}
|
|
486
|
+
if (cm.tool_result_max_chars !== undefined) {
|
|
487
|
+
const chars = cm.tool_result_max_chars;
|
|
488
|
+
if (typeof chars === "number" && chars >= 200 && chars <= 10000)
|
|
489
|
+
validCm.tool_result_max_chars = chars;
|
|
490
|
+
else
|
|
491
|
+
errors.push("context_management.tool_result_max_chars must be a number between 200 and 10000");
|
|
492
|
+
}
|
|
493
|
+
if (Object.keys(validCm).length > 0) {
|
|
494
|
+
validated.context_management = validCm;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
errors.push("context_management must be an object");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
455
501
|
// ─── Parallel Config ────────────────────────────────────────────────────
|
|
456
502
|
if (preferences.parallel && typeof preferences.parallel === "object") {
|
|
457
503
|
const p = preferences.parallel;
|
|
@@ -45,6 +45,13 @@ reason: "<reason>"
|
|
|
45
45
|
### Unpark a milestone
|
|
46
46
|
Remove the `{ID}-PARKED.md` file from the milestone directory to reactivate it.
|
|
47
47
|
|
|
48
|
+
### Skip a slice
|
|
49
|
+
Mark a slice as skipped so auto-mode advances past it without executing. Use the `gsd_skip_slice` tool:
|
|
50
|
+
```
|
|
51
|
+
gsd_skip_slice({ milestone_id: "M003", slice_id: "S02", reason: "Descoped — feature moved to M005" })
|
|
52
|
+
```
|
|
53
|
+
Skipped slices are treated as closed by the state machine (like "complete" but distinct). Use when a slice is no longer needed or has been superseded. The slice data is preserved for reference.
|
|
54
|
+
|
|
48
55
|
### Discard a milestone
|
|
49
56
|
**Permanently** delete a milestone directory and prune it from QUEUE-ORDER.json. **Always confirm with the user before discarding.** Warn explicitly if the milestone has completed work.
|
|
50
57
|
|
|
@@ -20,6 +20,8 @@ The user captured thoughts during execution using `/gsd capture`. Your job is to
|
|
|
20
20
|
|
|
21
21
|
For each capture, classify it as one of:
|
|
22
22
|
|
|
23
|
+
- **stop**: User directive to halt auto-mode immediately. Use when the user says "stop", "halt", "abort", "don't continue", "pause", or otherwise wants execution to cease. Auto-mode will pause after the current unit completes. Examples: "stop running", "halt execution", "don't continue".
|
|
24
|
+
- **backtrack**: User directive to abandon the current milestone and return to a previous one. The user believes earlier milestones missed critical features or need rework. Include the target milestone ID (e.g., M003) in the Resolution field. Auto-mode will pause and write a regression marker. Examples: "restart from M003", "go back to milestone 3", "M004 and M005 failed, restart from M003".
|
|
23
25
|
- **quick-task**: Small, self-contained, no downstream impact. Can be done in minutes without modifying the plan. Examples: fix a typo, add a missing import, tweak a config value.
|
|
24
26
|
- **inject**: Belongs in the current slice but wasn't planned. Needs a new task added to the slice plan. Examples: add error handling to a module being built, add a missing test case for current work.
|
|
25
27
|
- **defer**: Belongs in a future slice or milestone. Not urgent for current work. Examples: performance optimization, feature that depends on unbuilt infrastructure, nice-to-have enhancement.
|
|
@@ -28,10 +30,12 @@ For each capture, classify it as one of:
|
|
|
28
30
|
|
|
29
31
|
## Decision Guidelines
|
|
30
32
|
|
|
33
|
+
- **ALWAYS classify as stop** when the user explicitly says "stop", "halt", "abort", or "don't continue". Never shoe-horn a stop directive into "replan" or "note".
|
|
34
|
+
- **ALWAYS classify as backtrack** when the user references returning to a previous milestone, restarting from an earlier point, or abandoning current milestone work. Include the target milestone ID in the Resolution field (e.g., "Backtrack to M003").
|
|
31
35
|
- Prefer **quick-task** when the work is clearly small and self-contained.
|
|
32
36
|
- Prefer **inject** over **replan** when only a new task is needed, not rewriting existing ones.
|
|
33
37
|
- Prefer **defer** over **inject** when the work doesn't belong in the current slice's scope.
|
|
34
|
-
- Use **replan** only when remaining incomplete tasks need to change — not
|
|
38
|
+
- Use **replan** only when remaining incomplete tasks in the *current slice* need to change — not for cross-milestone issues.
|
|
35
39
|
- Use **note** for observations that don't require action.
|
|
36
40
|
- When unsure between quick-task and inject, consider: will this take more than 10 minutes? If yes, inject.
|
|
37
41
|
|
|
@@ -46,6 +50,7 @@ For each capture, classify it as one of:
|
|
|
46
50
|
- If applicable, which files would be affected
|
|
47
51
|
|
|
48
52
|
For captures classified as **note** or **defer**, auto-confirm without asking — these are low-impact.
|
|
53
|
+
For captures classified as **stop** or **backtrack**, auto-confirm without asking — these are urgent user directives that must be honored immediately.
|
|
49
54
|
For captures classified as **quick-task**, **inject**, or **replan**, ask the user to confirm or choose a different classification.
|
|
50
55
|
|
|
51
56
|
3. **Update** `.gsd/CAPTURES.md` — for each capture, update its section with the confirmed classification:
|
|
@@ -81,8 +81,11 @@ function buildRethinkData(basePath, milestoneIds, state, queueOrder) {
|
|
|
81
81
|
if (dbAvailable && status !== "complete") {
|
|
82
82
|
const slices = getMilestoneSlices(mid);
|
|
83
83
|
if (slices.length > 0) {
|
|
84
|
-
const done = slices.filter(s => s.status === "complete").length;
|
|
85
|
-
|
|
84
|
+
const done = slices.filter(s => s.status === "complete" || s.status === "done").length;
|
|
85
|
+
const skipped = slices.filter(s => s.status === "skipped").length;
|
|
86
|
+
sliceInfo = skipped > 0
|
|
87
|
+
? `${done}/${slices.length} complete, ${skipped} skipped`
|
|
88
|
+
: `${done}/${slices.length} complete`;
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
// Add parked reason if applicable
|
|
@@ -223,7 +223,7 @@ function extractContextTitle(content, fallback) {
|
|
|
223
223
|
* Helper: check if a DB status counts as "done" (handles K002 ambiguity).
|
|
224
224
|
*/
|
|
225
225
|
function isStatusDone(status) {
|
|
226
|
-
return status === 'complete' || status === 'done';
|
|
226
|
+
return status === 'complete' || status === 'done' || status === 'skipped';
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
229
|
* Derive GSD state from the milestones/slices/tasks DB tables.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Status predicates for GSD state-machine guards.
|
|
3
3
|
*
|
|
4
|
-
* The DB stores status as free-form strings.
|
|
5
|
-
* "closed": "complete" (canonical)
|
|
4
|
+
* The DB stores status as free-form strings. Three values indicate
|
|
5
|
+
* "closed": "complete" (canonical), "done" (legacy / alias), and
|
|
6
|
+
* "skipped" (user-directed skip via rethink or backtrack).
|
|
6
7
|
* Every inline `status === "complete" || status === "done"` should
|
|
7
8
|
* use isClosedStatus() instead.
|
|
8
9
|
*/
|
|
9
10
|
/** Returns true when a milestone, slice, or task status indicates closure. */
|
|
10
11
|
export function isClosedStatus(status) {
|
|
11
|
-
return status === "complete" || status === "done";
|
|
12
|
+
return status === "complete" || status === "done" || status === "skipped";
|
|
12
13
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Also provides detectFileOverlap() for surfacing downstream impact on quick tasks.
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { gsdRoot, milestonesDir } from "./paths.js";
|
|
@@ -99,6 +99,116 @@ export function executeReplan(basePath, mid, sid, capture) {
|
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
// ─── Backtrack (Milestone Regression) ────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Execute a backtrack directive — user wants to abandon current milestone
|
|
105
|
+
* and return to a previous one (milestone regression).
|
|
106
|
+
*
|
|
107
|
+
* Writes a BACKTRACK-TRIGGER.md marker at `.gsd/BACKTRACK-TRIGGER.md` with
|
|
108
|
+
* the target milestone, reason, and timestamp. The state machine (deriveState)
|
|
109
|
+
* detects this and transitions the project to the target milestone, resetting
|
|
110
|
+
* its slices to allow re-planning.
|
|
111
|
+
*
|
|
112
|
+
* Returns the extracted target milestone ID, or null if extraction failed.
|
|
113
|
+
*/
|
|
114
|
+
export function executeBacktrack(basePath, currentMilestoneId, capture) {
|
|
115
|
+
try {
|
|
116
|
+
// Extract target milestone from capture text or resolution
|
|
117
|
+
const targetMatch = (capture.resolution ?? capture.text)
|
|
118
|
+
.match(/\b(M\d{3}(?:-[a-z0-9]{6})?)\b/);
|
|
119
|
+
const targetMilestoneId = targetMatch?.[1] ?? null;
|
|
120
|
+
const ts = new Date().toISOString();
|
|
121
|
+
const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
|
|
122
|
+
const content = [
|
|
123
|
+
`# Backtrack Trigger`,
|
|
124
|
+
``,
|
|
125
|
+
`**Source:** Capture ${capture.id}`,
|
|
126
|
+
`**Capture:** ${capture.text}`,
|
|
127
|
+
`**Rationale:** ${capture.rationale ?? "User-initiated milestone backtrack"}`,
|
|
128
|
+
`**From:** ${currentMilestoneId}`,
|
|
129
|
+
`**Target:** ${targetMilestoneId ?? "(user to specify)"}`,
|
|
130
|
+
`**Triggered:** ${ts}`,
|
|
131
|
+
``,
|
|
132
|
+
`Auto-mode was paused by this backtrack directive. The user directed`,
|
|
133
|
+
`that the current milestone (${currentMilestoneId}) be abandoned and work`,
|
|
134
|
+
`should return to ${targetMilestoneId ?? "a previous milestone"}.`,
|
|
135
|
+
``,
|
|
136
|
+
`## Recovery Steps`,
|
|
137
|
+
``,
|
|
138
|
+
`1. Review what went wrong in ${currentMilestoneId}`,
|
|
139
|
+
`2. Identify missing features/requirements from the target milestone`,
|
|
140
|
+
`3. Resume auto-mode — the state machine will re-enter discussion for the target`,
|
|
141
|
+
].join("\n");
|
|
142
|
+
writeFileSync(triggerPath, content, "utf-8");
|
|
143
|
+
// If we have a valid target, also reset that milestone's completion status
|
|
144
|
+
// so deriveState() will re-enter it as the active milestone.
|
|
145
|
+
if (targetMilestoneId) {
|
|
146
|
+
try {
|
|
147
|
+
const targetDir = join(milestonesDir(basePath), targetMilestoneId);
|
|
148
|
+
if (existsSync(targetDir)) {
|
|
149
|
+
// Write a regression marker so the state machine knows this milestone
|
|
150
|
+
// needs re-discussion, not just re-execution
|
|
151
|
+
const regressionPath = join(targetDir, `${targetMilestoneId}-REGRESSION.md`);
|
|
152
|
+
writeFileSync(regressionPath, [
|
|
153
|
+
`# Milestone Regression`,
|
|
154
|
+
``,
|
|
155
|
+
`**From:** ${currentMilestoneId}`,
|
|
156
|
+
`**Reason:** ${capture.text}`,
|
|
157
|
+
`**Triggered:** ${ts}`,
|
|
158
|
+
``,
|
|
159
|
+
`This milestone is being revisited because downstream milestone`,
|
|
160
|
+
`${currentMilestoneId} failed or missed critical features that should`,
|
|
161
|
+
`have been part of this milestone's scope.`,
|
|
162
|
+
``,
|
|
163
|
+
`The discuss phase should re-evaluate requirements and identify gaps.`,
|
|
164
|
+
].join("\n"), "utf-8");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { /* best-effort */ }
|
|
168
|
+
}
|
|
169
|
+
return targetMilestoneId;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Read the backtrack trigger file if it exists.
|
|
177
|
+
* Returns the parsed target milestone and metadata, or null.
|
|
178
|
+
*/
|
|
179
|
+
export function readBacktrackTrigger(basePath) {
|
|
180
|
+
const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
|
|
181
|
+
if (!existsSync(triggerPath))
|
|
182
|
+
return null;
|
|
183
|
+
try {
|
|
184
|
+
const content = readFileSync(triggerPath, "utf-8");
|
|
185
|
+
const target = content.match(/\*\*Target:\*\*\s*(.+)/)?.[1]?.trim() ?? null;
|
|
186
|
+
const from = content.match(/\*\*From:\*\*\s*(.+)/)?.[1]?.trim() ?? null;
|
|
187
|
+
const capture = content.match(/\*\*Capture:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
188
|
+
const triggeredAt = content.match(/\*\*Triggered:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
189
|
+
return {
|
|
190
|
+
target: target === "(user to specify)" ? null : target,
|
|
191
|
+
from,
|
|
192
|
+
capture,
|
|
193
|
+
triggeredAt,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Remove the backtrack trigger after it has been processed.
|
|
202
|
+
*/
|
|
203
|
+
export function clearBacktrackTrigger(basePath) {
|
|
204
|
+
const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
|
|
205
|
+
try {
|
|
206
|
+
if (existsSync(triggerPath)) {
|
|
207
|
+
unlinkSync(triggerPath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch { /* best-effort */ }
|
|
211
|
+
}
|
|
102
212
|
// ─── File Overlap Detection ───────────────────────────────────────────────────
|
|
103
213
|
/**
|
|
104
214
|
* Detect file overlap between a capture's affected files and planned tasks.
|
|
@@ -240,6 +350,8 @@ export function executeTriageResolutions(basePath, mid, sid) {
|
|
|
240
350
|
replanned: 0,
|
|
241
351
|
deferredMilestones: 0,
|
|
242
352
|
quickTasks: [],
|
|
353
|
+
stopped: 0,
|
|
354
|
+
backtracks: [],
|
|
243
355
|
actions: [],
|
|
244
356
|
};
|
|
245
357
|
const actionable = loadActionableCaptures(basePath, mid || undefined);
|
|
@@ -318,5 +430,20 @@ export function executeTriageResolutions(basePath, mid, sid) {
|
|
|
318
430
|
}
|
|
319
431
|
}
|
|
320
432
|
}
|
|
433
|
+
// Count stop/backtrack captures — these are handled by the pre-dispatch guard
|
|
434
|
+
// in runGuards(), not here. We just report them for logging purposes.
|
|
435
|
+
const allCaptures = loadAllCaptures(basePath);
|
|
436
|
+
for (const cap of allCaptures) {
|
|
437
|
+
if (cap.status !== "resolved" || cap.executed)
|
|
438
|
+
continue;
|
|
439
|
+
if (cap.classification === "stop") {
|
|
440
|
+
result.stopped++;
|
|
441
|
+
result.actions.push(`Stop directive from ${cap.id}: "${cap.text}" — will pause on next dispatch`);
|
|
442
|
+
}
|
|
443
|
+
else if (cap.classification === "backtrack") {
|
|
444
|
+
result.backtracks.push(cap);
|
|
445
|
+
result.actions.push(`Backtrack directive from ${cap.id}: "${cap.text}" — will trigger milestone regression on next dispatch`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
321
448
|
return result;
|
|
322
449
|
}
|
|
@@ -33,9 +33,17 @@ const CLASSIFICATION_LABELS = {
|
|
|
33
33
|
label: "Note",
|
|
34
34
|
description: "Informational only — no action needed.",
|
|
35
35
|
},
|
|
36
|
+
"stop": {
|
|
37
|
+
label: "Stop",
|
|
38
|
+
description: "Halt auto-mode immediately — user directive to cease execution.",
|
|
39
|
+
},
|
|
40
|
+
"backtrack": {
|
|
41
|
+
label: "Backtrack",
|
|
42
|
+
description: "Abandon current milestone and return to a previous one.",
|
|
43
|
+
},
|
|
36
44
|
};
|
|
37
45
|
const ALL_CLASSIFICATIONS = [
|
|
38
|
-
"quick-task", "inject", "defer", "replan", "note",
|
|
46
|
+
"quick-task", "inject", "defer", "replan", "note", "stop", "backtrack",
|
|
39
47
|
];
|
|
40
48
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
41
49
|
/**
|
|
@@ -57,8 +65,9 @@ export async function showTriageConfirmation(ctx, triageResults, captures, baseP
|
|
|
57
65
|
const capture = captureMap.get(result.captureId);
|
|
58
66
|
if (!capture)
|
|
59
67
|
continue;
|
|
60
|
-
// Auto-confirm note and
|
|
61
|
-
if (result.classification === "note" || result.classification === "defer"
|
|
68
|
+
// Auto-confirm note, defer, stop, and backtrack — low-impact or urgent directives
|
|
69
|
+
if (result.classification === "note" || result.classification === "defer"
|
|
70
|
+
|| result.classification === "stop" || result.classification === "backtrack") {
|
|
62
71
|
const resolution = result.classification === "note"
|
|
63
72
|
? "acknowledged as note"
|
|
64
73
|
: `deferred${result.targetSlice ? ` to ${result.targetSlice}` : ""}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
t_cBZAENjaOJIRST3dw08
|
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
{
|
|
2
|
-
"/_not-found/page": "/_not-found",
|
|
3
2
|
"/_global-error/page": "/_global-error",
|
|
3
|
+
"/_not-found/page": "/_not-found",
|
|
4
4
|
"/api/boot/route": "/api/boot",
|
|
5
5
|
"/api/bridge-terminal/input/route": "/api/bridge-terminal/input",
|
|
6
6
|
"/api/bridge-terminal/resize/route": "/api/bridge-terminal/resize",
|
|
7
7
|
"/api/dev-mode/route": "/api/dev-mode",
|
|
8
|
-
"/api/browse-directories/route": "/api/browse-directories",
|
|
9
8
|
"/api/doctor/route": "/api/doctor",
|
|
9
|
+
"/api/captures/route": "/api/captures",
|
|
10
10
|
"/api/cleanup/route": "/api/cleanup",
|
|
11
|
-
"/api/
|
|
12
|
-
"/api/bridge-terminal/stream/route": "/api/bridge-terminal/stream",
|
|
11
|
+
"/api/browse-directories/route": "/api/browse-directories",
|
|
13
12
|
"/api/forensics/route": "/api/forensics",
|
|
14
|
-
"/api/
|
|
15
|
-
"/api/history/route": "/api/history",
|
|
13
|
+
"/api/export-data/route": "/api/export-data",
|
|
16
14
|
"/api/git/route": "/api/git",
|
|
17
15
|
"/api/hooks/route": "/api/hooks",
|
|
16
|
+
"/api/history/route": "/api/history",
|
|
17
|
+
"/api/knowledge/route": "/api/knowledge",
|
|
18
18
|
"/api/inspect/route": "/api/inspect",
|
|
19
19
|
"/api/experimental/route": "/api/experimental",
|
|
20
|
-
"/api/knowledge/route": "/api/knowledge",
|
|
21
20
|
"/api/live-state/route": "/api/live-state",
|
|
22
|
-
"/api/
|
|
23
|
-
"/api/onboarding/route": "/api/onboarding",
|
|
21
|
+
"/api/bridge-terminal/stream/route": "/api/bridge-terminal/stream",
|
|
24
22
|
"/api/preferences/route": "/api/preferences",
|
|
23
|
+
"/api/recovery/route": "/api/recovery",
|
|
25
24
|
"/api/projects/route": "/api/projects",
|
|
26
25
|
"/api/session/browser/route": "/api/session/browser",
|
|
27
26
|
"/api/session/command/route": "/api/session/command",
|
|
28
|
-
"/api/
|
|
27
|
+
"/api/onboarding/route": "/api/onboarding",
|
|
28
|
+
"/api/session/events/route": "/api/session/events",
|
|
29
29
|
"/api/settings-data/route": "/api/settings-data",
|
|
30
30
|
"/api/shutdown/route": "/api/shutdown",
|
|
31
|
-
"/api/session/events/route": "/api/session/events",
|
|
32
|
-
"/api/steer/route": "/api/steer",
|
|
33
31
|
"/api/skill-health/route": "/api/skill-health",
|
|
32
|
+
"/api/session/manage/route": "/api/session/manage",
|
|
34
33
|
"/api/terminal/input/route": "/api/terminal/input",
|
|
35
34
|
"/api/switch-root/route": "/api/switch-root",
|
|
36
35
|
"/api/terminal/resize/route": "/api/terminal/resize",
|
|
36
|
+
"/api/steer/route": "/api/steer",
|
|
37
37
|
"/api/terminal/stream/route": "/api/terminal/stream",
|
|
38
38
|
"/api/remote-questions/route": "/api/remote-questions",
|
|
39
|
-
"/api/terminal/upload/route": "/api/terminal/upload",
|
|
40
39
|
"/api/terminal/sessions/route": "/api/terminal/sessions",
|
|
41
40
|
"/api/visualizer/route": "/api/visualizer",
|
|
41
|
+
"/api/undo/route": "/api/undo",
|
|
42
|
+
"/api/terminal/upload/route": "/api/terminal/upload",
|
|
42
43
|
"/api/update/route": "/api/update",
|
|
43
44
|
"/api/files/route": "/api/files",
|
|
44
|
-
"/api/undo/route": "/api/undo",
|
|
45
45
|
"/page": "/"
|
|
46
46
|
}
|