gsd-pi 2.62.0 → 2.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/extensions/ask-user-questions.js +47 -3
- package/dist/resources/extensions/gsd/auto-start.js +11 -6
- package/dist/resources/extensions/gsd/auto-timers.js +8 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
- package/dist/resources/extensions/gsd/auto.js +24 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +18 -7
- package/dist/resources/extensions/gsd/db-writer.js +64 -28
- package/dist/resources/extensions/gsd/preferences-models.js +74 -0
- package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
- package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
- package/dist/resources/extensions/gsd/skill-health.js +7 -3
- package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- 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 +16 -16
- 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/packages/mcp-server/src/cli.ts +1 -1
- package/packages/mcp-server/src/index.ts +15 -1
- package/packages/mcp-server/src/readers/captures.ts +119 -0
- package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
- package/packages/mcp-server/src/readers/index.ts +16 -0
- package/packages/mcp-server/src/readers/knowledge.ts +111 -0
- package/packages/mcp-server/src/readers/metrics.ts +118 -0
- package/packages/mcp-server/src/readers/paths.ts +217 -0
- package/packages/mcp-server/src/readers/readers.test.ts +509 -0
- package/packages/mcp-server/src/readers/roadmap.ts +263 -0
- package/packages/mcp-server/src/readers/state.ts +223 -0
- package/packages/mcp-server/src/server.ts +134 -3
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
- package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +60 -4
- package/src/resources/extensions/gsd/auto-start.ts +11 -6
- package/src/resources/extensions/gsd/auto-timers.ts +8 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
- package/src/resources/extensions/gsd/auto.ts +25 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +20 -7
- package/src/resources/extensions/gsd/db-writer.ts +67 -30
- package/src/resources/extensions/gsd/preferences-models.ts +78 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
- package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
- package/src/resources/extensions/gsd/skill-health.ts +7 -3
- package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
- package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
- package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
- package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
- /package/dist/web/standalone/.next/static/{F4rzqt_3m83A68ZRiU12r → 86gWhNPP3233lZ7KPwda7}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{F4rzqt_3m83A68ZRiU12r → 86gWhNPP3233lZ7KPwda7}/_ssgManifest.js +0 -0
|
@@ -33,6 +33,31 @@ const AskUserQuestionsParams = Type.Object({
|
|
|
33
33
|
description: "Questions to show the user. Prefer 1 and do not exceed 3.",
|
|
34
34
|
}),
|
|
35
35
|
});
|
|
36
|
+
// ─── Per-turn deduplication ──────────────────────────────────────────────────
|
|
37
|
+
// Prevents duplicate question dispatches (especially to remote channels like
|
|
38
|
+
// Discord) when the LLM calls ask_user_questions multiple times with the same
|
|
39
|
+
// questions in a single turn. Keyed by full canonicalized payload (id, header,
|
|
40
|
+
// question, options, allowMultiple) — not just IDs — so that calls with the
|
|
41
|
+
// same IDs but different text/options are treated as distinct.
|
|
42
|
+
import { createHash } from "node:crypto";
|
|
43
|
+
const turnCache = new Map();
|
|
44
|
+
/** @internal Exported for testing only. */
|
|
45
|
+
export function questionSignature(questions) {
|
|
46
|
+
const canonical = questions
|
|
47
|
+
.map((q) => ({
|
|
48
|
+
id: q.id,
|
|
49
|
+
header: q.header,
|
|
50
|
+
question: q.question,
|
|
51
|
+
options: (q.options || []).map((o) => ({ label: o.label, description: o.description })),
|
|
52
|
+
allowMultiple: !!q.allowMultiple,
|
|
53
|
+
}))
|
|
54
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
55
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
|
56
|
+
}
|
|
57
|
+
/** Reset the dedup cache. Called on session boundaries. */
|
|
58
|
+
export function resetAskUserQuestionsCache() {
|
|
59
|
+
turnCache.clear();
|
|
60
|
+
}
|
|
36
61
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
37
62
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
38
63
|
function errorResult(message, questions = []) {
|
|
@@ -73,6 +98,15 @@ export default function AskUserQuestions(pi) {
|
|
|
73
98
|
],
|
|
74
99
|
parameters: AskUserQuestionsParams,
|
|
75
100
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
101
|
+
// ── Per-turn dedup: return cached result for identical question sets ──
|
|
102
|
+
const sig = questionSignature(params.questions);
|
|
103
|
+
const cached = turnCache.get(sig);
|
|
104
|
+
if (cached) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: cached.content[0].text + "\n(Returned cached answer — this question set was already asked this turn.)" }],
|
|
107
|
+
details: cached.details,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
76
110
|
// Validation
|
|
77
111
|
if (params.questions.length === 0 || params.questions.length > 3) {
|
|
78
112
|
return errorResult("Error: questions must contain 1-3 items", params.questions);
|
|
@@ -87,8 +121,14 @@ export default function AskUserQuestions(pi) {
|
|
|
87
121
|
// this is a no-op when the user has not set up Slack/Discord/Telegram.
|
|
88
122
|
const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
|
|
89
123
|
const remoteResult = await tryRemoteQuestions(params.questions, signal);
|
|
90
|
-
if (remoteResult)
|
|
124
|
+
if (remoteResult) {
|
|
125
|
+
// Cache successful remote results to prevent duplicate Discord dispatches
|
|
126
|
+
const remoteDetails = remoteResult.details;
|
|
127
|
+
if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
|
|
128
|
+
turnCache.set(sig, remoteResult);
|
|
129
|
+
}
|
|
91
130
|
return { ...remoteResult, details: remoteResult.details };
|
|
131
|
+
}
|
|
92
132
|
if (!ctx.hasUI) {
|
|
93
133
|
return errorResult("Error: UI not available (non-interactive mode)", params.questions);
|
|
94
134
|
}
|
|
@@ -131,7 +171,7 @@ export default function AskUserQuestions(pi) {
|
|
|
131
171
|
{ selected: a.answers.length === 1 ? a.answers[0] : a.answers, notes: "" },
|
|
132
172
|
])),
|
|
133
173
|
};
|
|
134
|
-
|
|
174
|
+
const fallbackResult = {
|
|
135
175
|
content: [{ type: "text", text: JSON.stringify({ answers }) }],
|
|
136
176
|
details: {
|
|
137
177
|
questions: params.questions,
|
|
@@ -139,6 +179,8 @@ export default function AskUserQuestions(pi) {
|
|
|
139
179
|
cancelled: false,
|
|
140
180
|
},
|
|
141
181
|
};
|
|
182
|
+
turnCache.set(sig, fallbackResult);
|
|
183
|
+
return fallbackResult;
|
|
142
184
|
}
|
|
143
185
|
// Check if cancelled (empty answers = user exited)
|
|
144
186
|
const hasAnswers = Object.keys(result.answers).length > 0;
|
|
@@ -148,10 +190,12 @@ export default function AskUserQuestions(pi) {
|
|
|
148
190
|
details: { questions: params.questions, response: null, cancelled: true },
|
|
149
191
|
};
|
|
150
192
|
}
|
|
151
|
-
|
|
193
|
+
const successResult = {
|
|
152
194
|
content: [{ type: "text", text: formatForLLM(result) }],
|
|
153
195
|
details: { questions: params.questions, response: result, cancelled: false },
|
|
154
196
|
};
|
|
197
|
+
turnCache.set(sig, successResult);
|
|
198
|
+
return successResult;
|
|
155
199
|
},
|
|
156
200
|
// ─── Rendering ────────────────────────────────────────────────────────
|
|
157
201
|
renderCall(args, theme) {
|
|
@@ -39,6 +39,7 @@ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:
|
|
|
39
39
|
import { join } from "node:path";
|
|
40
40
|
import { sep as pathSep } from "node:path";
|
|
41
41
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
42
|
+
import { resolveDefaultSessionModel } from "./preferences-models.js";
|
|
42
43
|
/**
|
|
43
44
|
* Bootstrap a fresh auto-mode session. Handles everything from git init
|
|
44
45
|
* through secrets collection, returning when ready for the first
|
|
@@ -89,12 +90,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
89
90
|
}
|
|
90
91
|
// Capture the user's session model before guided-flow dispatch can apply a
|
|
91
92
|
// phase-specific planning model for a discuss turn (#2829).
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
//
|
|
94
|
+
// GSD PREFERENCES.md takes priority over the session model from settings.json
|
|
95
|
+
// (#3517). The session model (ctx.model) comes from findInitialModel() which
|
|
96
|
+
// reads defaultProvider/defaultModel from ~/.gsd/agent/settings.json. When
|
|
97
|
+
// the user has explicit model preferences in PREFERENCES.md, those should win.
|
|
98
|
+
const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
|
|
99
|
+
const startModelSnapshot = preferredModel
|
|
100
|
+
?? (ctx.model
|
|
101
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
102
|
+
: null);
|
|
98
103
|
try {
|
|
99
104
|
// Validate GSD_PROJECT_ID early so the user gets immediate feedback
|
|
100
105
|
const customProjectId = process.env.GSD_PROJECT_ID;
|
|
@@ -92,6 +92,10 @@ export function startUnitSupervision(sctx) {
|
|
|
92
92
|
phase: "wrapup-warning-sent",
|
|
93
93
|
wrapupWarningSent: true,
|
|
94
94
|
});
|
|
95
|
+
// Only trigger a new turn if no tools are currently in flight.
|
|
96
|
+
// Triggering during active tool calls causes tool results to be skipped
|
|
97
|
+
// with "Skipped due to queued user message", leading to provider errors (#3512).
|
|
98
|
+
const softTrigger = getInFlightToolCount() === 0;
|
|
95
99
|
pi.sendMessage({
|
|
96
100
|
customType: "gsd-auto-wrapup",
|
|
97
101
|
display: s.verbose,
|
|
@@ -104,7 +108,7 @@ export function startUnitSupervision(sctx) {
|
|
|
104
108
|
"3. mark task or slice state on disk correctly",
|
|
105
109
|
"4. leave precise resume notes if anything remains unfinished",
|
|
106
110
|
].join("\n"),
|
|
107
|
-
}, { triggerTurn:
|
|
111
|
+
}, { triggerTurn: softTrigger });
|
|
108
112
|
}, softTimeoutMs);
|
|
109
113
|
// ── 2. Idle watchdog ──
|
|
110
114
|
s.idleWatchdogHandle = setInterval(async () => {
|
|
@@ -245,6 +249,8 @@ export function startUnitSupervision(sctx) {
|
|
|
245
249
|
if (s.verbose) {
|
|
246
250
|
ctx.ui.notify(`Context at ${contextUsage.percent}% (threshold: ${continueHereThreshold}%) — sending wrap-up signal.`, "info");
|
|
247
251
|
}
|
|
252
|
+
// Only trigger a new turn if no tools are currently in flight (#3512).
|
|
253
|
+
const contextTrigger = getInFlightToolCount() === 0;
|
|
248
254
|
pi.sendMessage({
|
|
249
255
|
customType: "gsd-auto-wrapup",
|
|
250
256
|
display: s.verbose,
|
|
@@ -258,7 +264,7 @@ export function startUnitSupervision(sctx) {
|
|
|
258
264
|
"4. Leave precise resume notes if anything remains unfinished",
|
|
259
265
|
"Do NOT start new sub-tasks or investigations.",
|
|
260
266
|
].join("\n"),
|
|
261
|
-
}, { triggerTurn:
|
|
267
|
+
}, { triggerTurn: contextTrigger });
|
|
262
268
|
if (s.continueHereHandle) {
|
|
263
269
|
clearInterval(s.continueHereHandle);
|
|
264
270
|
s.continueHereHandle = null;
|
|
@@ -234,10 +234,29 @@ export function syncProjectRootToWorktree(projectRoot, worktreePath_, milestoneI
|
|
|
234
234
|
// openDatabase re-creates it, causing "no such table" failures (#2815).
|
|
235
235
|
try {
|
|
236
236
|
const wtDb = join(wtGsd, "gsd.db");
|
|
237
|
+
let deleteSidecars = false;
|
|
237
238
|
if (existsSync(wtDb)) {
|
|
238
239
|
const size = statSync(wtDb).size;
|
|
239
240
|
if (size === 0) {
|
|
240
241
|
unlinkSync(wtDb);
|
|
242
|
+
deleteSidecars = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Main DB already missing — sidecars are orphaned from a previous
|
|
247
|
+
// partial cleanup and must still be removed.
|
|
248
|
+
deleteSidecars = true;
|
|
249
|
+
}
|
|
250
|
+
// Always clean up WAL/SHM sidecar files when the main DB was deleted
|
|
251
|
+
// or is already missing. Orphaned WAL/SHM files cause SQLite WAL
|
|
252
|
+
// recovery on next open, which triggers a CPU spin on Node 24's
|
|
253
|
+
// node:sqlite DatabaseSync implementation (#2478).
|
|
254
|
+
if (deleteSidecars) {
|
|
255
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
256
|
+
const f = wtDb + suffix;
|
|
257
|
+
if (existsSync(f)) {
|
|
258
|
+
unlinkSync(f);
|
|
259
|
+
}
|
|
241
260
|
}
|
|
242
261
|
}
|
|
243
262
|
}
|
|
@@ -377,6 +377,18 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
377
377
|
catch (e) {
|
|
378
378
|
debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
|
|
379
379
|
}
|
|
380
|
+
// ── Step 1b: Flush queued follow-up messages (#3512) ──
|
|
381
|
+
// Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
|
|
382
|
+
// extra LLM turns after stop. Flush them the same way run-unit.ts does.
|
|
383
|
+
try {
|
|
384
|
+
const cmdCtxAny = s.cmdCtx;
|
|
385
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
386
|
+
cmdCtxAny.clearQueue();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
debugLog("stop-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
|
|
391
|
+
}
|
|
380
392
|
// ── Step 2: Skill state ──
|
|
381
393
|
try {
|
|
382
394
|
clearSkillSnapshot();
|
|
@@ -589,6 +601,18 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
589
601
|
if (!s.active)
|
|
590
602
|
return;
|
|
591
603
|
clearUnitTimeout();
|
|
604
|
+
// Flush queued follow-up messages (#3512).
|
|
605
|
+
// Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
|
|
606
|
+
// extra LLM turns after pause. Flush them the same way run-unit.ts does.
|
|
607
|
+
try {
|
|
608
|
+
const cmdCtxAny = s.cmdCtx;
|
|
609
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
610
|
+
cmdCtxAny.clearQueue();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch (e) {
|
|
614
|
+
debugLog("pause-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
|
|
615
|
+
}
|
|
592
616
|
// Unblock any pending unit promise so the auto-loop is not orphaned.
|
|
593
617
|
// Pass errorContext so runUnitPhase can distinguish user-initiated pause
|
|
594
618
|
// from provider-error pause and avoid hard-stopping (#2762).
|
|
@@ -14,6 +14,7 @@ import { getAutoDashboardData, isAutoActive, isAutoPaused, markToolEnd, markTool
|
|
|
14
14
|
import { isParallelActive, shutdownParallel } from "../parallel-orchestrator.js";
|
|
15
15
|
import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
|
|
16
16
|
import { saveActivityLog } from "../activity-log.js";
|
|
17
|
+
import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
|
|
17
18
|
// Skip the welcome screen on the very first session_start — cli.ts already
|
|
18
19
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
19
20
|
let isFirstSession = true;
|
|
@@ -25,6 +26,7 @@ export function registerHooks(pi) {
|
|
|
25
26
|
pi.on("session_start", async (_event, ctx) => {
|
|
26
27
|
resetWriteGateState();
|
|
27
28
|
resetToolCallLoopGuard();
|
|
29
|
+
resetAskUserQuestionsCache();
|
|
28
30
|
await syncServiceTierStatus(ctx);
|
|
29
31
|
// Apply show_token_cost preference (#1515)
|
|
30
32
|
try {
|
|
@@ -60,6 +62,7 @@ export function registerHooks(pi) {
|
|
|
60
62
|
pi.on("session_switch", async (_event, ctx) => {
|
|
61
63
|
resetWriteGateState();
|
|
62
64
|
resetToolCallLoopGuard();
|
|
65
|
+
resetAskUserQuestionsCache();
|
|
63
66
|
clearDiscussionFlowState();
|
|
64
67
|
await syncServiceTierStatus(ctx);
|
|
65
68
|
loadToolApiKeys();
|
|
@@ -69,6 +72,7 @@ export function registerHooks(pi) {
|
|
|
69
72
|
});
|
|
70
73
|
pi.on("agent_end", async (event, ctx) => {
|
|
71
74
|
resetToolCallLoopGuard();
|
|
75
|
+
resetAskUserQuestionsCache();
|
|
72
76
|
await handleAgentEnd(pi, event, ctx);
|
|
73
77
|
});
|
|
74
78
|
// Squash-merge quick-task branch back to the original branch after the
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
|
|
16
|
+
/** Interactive/user-facing tools where even 1 duplicate is confusing. */
|
|
17
|
+
const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
|
|
18
|
+
const MAX_CONSECUTIVE_STRICT = 1;
|
|
16
19
|
let consecutiveCount = 0;
|
|
17
20
|
let lastSignature = "";
|
|
21
|
+
let lastToolName = "";
|
|
18
22
|
let enabled = true;
|
|
19
23
|
/** Hash tool name + args into a compact signature for comparison. */
|
|
20
24
|
function hashToolCall(toolName, args) {
|
|
@@ -45,8 +49,12 @@ export function checkToolCallLoop(toolName, args) {
|
|
|
45
49
|
else {
|
|
46
50
|
consecutiveCount = 1;
|
|
47
51
|
lastSignature = sig;
|
|
52
|
+
lastToolName = toolName;
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
const threshold = STRICT_LOOP_TOOLS.has(toolName)
|
|
55
|
+
? MAX_CONSECUTIVE_STRICT
|
|
56
|
+
: MAX_CONSECUTIVE_IDENTICAL_CALLS;
|
|
57
|
+
if (consecutiveCount > threshold) {
|
|
50
58
|
return {
|
|
51
59
|
block: true,
|
|
52
60
|
reason: `Tool loop detected: ${toolName} called ${consecutiveCount} times ` +
|
|
@@ -61,6 +69,7 @@ export function checkToolCallLoop(toolName, args) {
|
|
|
61
69
|
export function resetToolCallLoopGuard() {
|
|
62
70
|
consecutiveCount = 0;
|
|
63
71
|
lastSignature = "";
|
|
72
|
+
lastToolName = "";
|
|
64
73
|
enabled = true;
|
|
65
74
|
}
|
|
66
75
|
/** Disable the guard (e.g. during shutdown). */
|
|
@@ -68,6 +77,7 @@ export function disableToolCallLoopGuard() {
|
|
|
68
77
|
enabled = false;
|
|
69
78
|
consecutiveCount = 0;
|
|
70
79
|
lastSignature = "";
|
|
80
|
+
lastToolName = "";
|
|
71
81
|
}
|
|
72
82
|
/** Get current consecutive count for diagnostics. */
|
|
73
83
|
export function getToolCallLoopCount() {
|
|
@@ -11,7 +11,8 @@ import { gsdRoot } from "./paths.js";
|
|
|
11
11
|
import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
|
|
12
12
|
import { appendOverride, appendKnowledge } from "./files.js";
|
|
13
13
|
import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
|
|
14
|
-
import { isAutoActive } from "./auto.js";
|
|
14
|
+
import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
|
|
15
|
+
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
15
16
|
import { projectRoot } from "./commands/context.js";
|
|
16
17
|
import { loadPrompt } from "./prompt-loader.js";
|
|
17
18
|
export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
@@ -172,7 +173,17 @@ export async function handleSteer(change, ctx, pi) {
|
|
|
172
173
|
const sid = state.activeSlice?.id ?? "none";
|
|
173
174
|
const tid = state.activeTask?.id ?? "none";
|
|
174
175
|
const appliedAt = `${mid}/${sid}/${tid}`;
|
|
175
|
-
|
|
176
|
+
// Resolve the correct target path: only route to a worktree when auto-mode
|
|
177
|
+
// is actively running there (in-process or remote). A worktree directory may
|
|
178
|
+
// exist from a previous session without being the active runtime path —
|
|
179
|
+
// writing there without a live session would silently drop the override.
|
|
180
|
+
const autoRunning = isAutoActive() || checkRemoteAutoSession(basePath).running;
|
|
181
|
+
const wtPath = autoRunning && mid !== "none"
|
|
182
|
+
? getAutoWorktreePath(basePath, mid)
|
|
183
|
+
: null;
|
|
184
|
+
const targetPath = wtPath ?? basePath;
|
|
185
|
+
await appendOverride(targetPath, change, appliedAt);
|
|
186
|
+
const overrideLoc = wtPath ? "worktree `.gsd/OVERRIDES.md`" : "`.gsd/OVERRIDES.md`";
|
|
176
187
|
if (isAutoActive()) {
|
|
177
188
|
pi.sendMessage({
|
|
178
189
|
customType: "gsd-hard-steer",
|
|
@@ -181,14 +192,14 @@ export async function handleSteer(change, ctx, pi) {
|
|
|
181
192
|
"",
|
|
182
193
|
`**Override:** ${change}`,
|
|
183
194
|
"",
|
|
184
|
-
|
|
195
|
+
`This override has been saved to ${overrideLoc} and will be injected into all future task prompts.`,
|
|
185
196
|
"A document rewrite unit will run before the next task to propagate this change across all active plan documents.",
|
|
186
197
|
"",
|
|
187
198
|
"If you are mid-task, finish your current work respecting this override. The next dispatched unit will be a document rewrite.",
|
|
188
199
|
].join("\n"),
|
|
189
200
|
display: false,
|
|
190
201
|
}, { triggerTurn: true });
|
|
191
|
-
ctx.ui.notify(`Override registered: "${change}". Will be applied before next task dispatch.`, "info");
|
|
202
|
+
ctx.ui.notify(`Override registered (${overrideLoc}): "${change}". Will be applied before next task dispatch.`, "info");
|
|
192
203
|
}
|
|
193
204
|
else {
|
|
194
205
|
pi.sendMessage({
|
|
@@ -198,13 +209,13 @@ export async function handleSteer(change, ctx, pi) {
|
|
|
198
209
|
"",
|
|
199
210
|
`**Override:** ${change}`,
|
|
200
211
|
"",
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
`This override has been saved to ${overrideLoc}.`,
|
|
213
|
+
`Before continuing, read ${overrideLoc} and update the current plan documents to reflect this change.`,
|
|
203
214
|
"Focus on: active slice plan, incomplete task plans, and DECISIONS.md.",
|
|
204
215
|
].join("\n"),
|
|
205
216
|
display: false,
|
|
206
217
|
}, { triggerTurn: true });
|
|
207
|
-
ctx.ui.notify(`Override registered: "${change}". Update plan documents to reflect this change.`, "info");
|
|
218
|
+
ctx.ui.notify(`Override registered (${overrideLoc}): "${change}". Update plan documents to reflect this change.`, "info");
|
|
208
219
|
}
|
|
209
220
|
}
|
|
210
221
|
export async function handleKnowledge(args, ctx) {
|
|
@@ -11,7 +11,7 @@ import { resolve } from 'node:path';
|
|
|
11
11
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
12
12
|
import { resolveGsdRootFile } from './paths.js';
|
|
13
13
|
import { saveFile } from './files.js';
|
|
14
|
-
import { GSDError, GSD_IO_ERROR } from './errors.js';
|
|
14
|
+
import { GSDError, GSD_STALE_STATE, GSD_IO_ERROR } from './errors.js';
|
|
15
15
|
import { logWarning, logError } from './workflow-logger.js';
|
|
16
16
|
import { invalidateStateCache } from './state.js';
|
|
17
17
|
import { clearPathCache } from './paths.js';
|
|
@@ -234,27 +234,44 @@ export async function nextRequirementId() {
|
|
|
234
234
|
/**
|
|
235
235
|
* Save a new requirement to DB and regenerate REQUIREMENTS.md.
|
|
236
236
|
* Auto-assigns the next ID via nextRequirementId().
|
|
237
|
+
*
|
|
238
|
+
* The ID computation and insert are wrapped in a single transaction
|
|
239
|
+
* to prevent parallel race conditions (same pattern as saveDecisionToDb).
|
|
240
|
+
*
|
|
237
241
|
* Returns the assigned ID.
|
|
238
242
|
*/
|
|
239
243
|
export async function saveRequirementToDb(fields, basePath) {
|
|
240
244
|
try {
|
|
241
245
|
const db = await import('./gsd-db.js');
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
246
|
+
// Atomic ID assignment + insert inside a transaction.
|
|
247
|
+
const id = db.transaction(() => {
|
|
248
|
+
const adapter = db._getAdapter();
|
|
249
|
+
if (!adapter)
|
|
250
|
+
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
251
|
+
const row = adapter
|
|
252
|
+
.prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
|
|
253
|
+
.get();
|
|
254
|
+
const maxNum = row ? row['max_num'] : null;
|
|
255
|
+
const nextId = (maxNum == null || isNaN(maxNum))
|
|
256
|
+
? 'R001'
|
|
257
|
+
: `R${String(maxNum + 1).padStart(3, '0')}`;
|
|
258
|
+
const requirement = {
|
|
259
|
+
id: nextId,
|
|
260
|
+
class: fields.class,
|
|
261
|
+
status: fields.status ?? 'active',
|
|
262
|
+
description: fields.description,
|
|
263
|
+
why: fields.why,
|
|
264
|
+
source: fields.source,
|
|
265
|
+
primary_owner: fields.primary_owner ?? '',
|
|
266
|
+
supporting_slices: fields.supporting_slices ?? '',
|
|
267
|
+
validation: fields.validation ?? '',
|
|
268
|
+
notes: fields.notes ?? '',
|
|
269
|
+
full_content: '',
|
|
270
|
+
superseded_by: null,
|
|
271
|
+
};
|
|
272
|
+
db.upsertRequirement(requirement);
|
|
273
|
+
return nextId;
|
|
274
|
+
});
|
|
258
275
|
// Fetch all requirements for full file regeneration
|
|
259
276
|
const adapter = db._getAdapter();
|
|
260
277
|
let allRequirements = [];
|
|
@@ -300,22 +317,41 @@ export async function saveRequirementToDb(fields, basePath) {
|
|
|
300
317
|
/**
|
|
301
318
|
* Save a new decision to DB and regenerate DECISIONS.md.
|
|
302
319
|
* Auto-assigns the next ID via nextDecisionId().
|
|
320
|
+
*
|
|
321
|
+
* The ID computation (SELECT MAX) and insert are wrapped in a single
|
|
322
|
+
* transaction to prevent parallel tool calls from computing the same ID
|
|
323
|
+
* and silently overwriting each other (#3326, #3339, #3459).
|
|
324
|
+
*
|
|
303
325
|
* Returns the assigned ID.
|
|
304
326
|
*/
|
|
305
327
|
export async function saveDecisionToDb(fields, basePath) {
|
|
306
328
|
try {
|
|
307
329
|
const db = await import('./gsd-db.js');
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
// Atomic ID assignment + insert inside a transaction to prevent
|
|
331
|
+
// parallel calls from racing on the same MAX(id) value.
|
|
332
|
+
const id = db.transaction(() => {
|
|
333
|
+
const adapter = db._getAdapter();
|
|
334
|
+
if (!adapter)
|
|
335
|
+
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
336
|
+
const row = adapter
|
|
337
|
+
.prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM decisions')
|
|
338
|
+
.get();
|
|
339
|
+
const maxNum = row ? row['max_num'] : null;
|
|
340
|
+
const nextId = (maxNum == null || isNaN(maxNum))
|
|
341
|
+
? 'D001'
|
|
342
|
+
: `D${String(maxNum + 1).padStart(3, '0')}`;
|
|
343
|
+
db.upsertDecision({
|
|
344
|
+
id: nextId,
|
|
345
|
+
when_context: fields.when_context ?? '',
|
|
346
|
+
scope: fields.scope,
|
|
347
|
+
decision: fields.decision,
|
|
348
|
+
choice: fields.choice,
|
|
349
|
+
rationale: fields.rationale,
|
|
350
|
+
revisable: fields.revisable ?? 'Yes',
|
|
351
|
+
made_by: fields.made_by ?? 'agent',
|
|
352
|
+
superseded_by: null,
|
|
353
|
+
});
|
|
354
|
+
return nextId;
|
|
319
355
|
});
|
|
320
356
|
// Fetch all decisions (including superseded for the full register)
|
|
321
357
|
const adapter = db._getAdapter();
|
|
@@ -87,6 +87,80 @@ export function resolveModelWithFallbacksForUnit(unitType) {
|
|
|
87
87
|
fallbacks: phaseConfig.fallbacks ?? [],
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the default session model from GSD preferences.
|
|
92
|
+
*
|
|
93
|
+
* Used at auto-mode bootstrap to override the session model that was
|
|
94
|
+
* determined by settings.json (defaultProvider/defaultModel). When
|
|
95
|
+
* PREFERENCES.md (or project preferences) configures an `execution` model
|
|
96
|
+
* we treat that as the session default. Falls back through execution →
|
|
97
|
+
* planning → first configured model.
|
|
98
|
+
*
|
|
99
|
+
* Accepts an optional `sessionProvider` for bare model IDs that don't
|
|
100
|
+
* include an explicit provider prefix (e.g. `gpt-5.4` instead of
|
|
101
|
+
* `openai-codex/gpt-5.4`). When a bare ID is found and sessionProvider
|
|
102
|
+
* is available, the session provider is used. Without sessionProvider,
|
|
103
|
+
* bare IDs are still returned with provider set to the bare ID itself
|
|
104
|
+
* so downstream resolution (resolveModelId) can match it.
|
|
105
|
+
*
|
|
106
|
+
* Returns `{ provider, id }` or `undefined` if no model preference is
|
|
107
|
+
* configured.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveDefaultSessionModel(sessionProvider) {
|
|
110
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
111
|
+
if (!prefs?.preferences.models)
|
|
112
|
+
return undefined;
|
|
113
|
+
const m = prefs.preferences.models;
|
|
114
|
+
// Priority: execution → planning → first configured value
|
|
115
|
+
const candidates = [
|
|
116
|
+
m.execution,
|
|
117
|
+
m.planning,
|
|
118
|
+
m.research,
|
|
119
|
+
m.discuss,
|
|
120
|
+
m.completion,
|
|
121
|
+
m.validation,
|
|
122
|
+
m.subagent,
|
|
123
|
+
];
|
|
124
|
+
for (const cfg of candidates) {
|
|
125
|
+
if (!cfg)
|
|
126
|
+
continue;
|
|
127
|
+
// Normalize to provider + id from the various config shapes
|
|
128
|
+
let provider;
|
|
129
|
+
let id;
|
|
130
|
+
if (typeof cfg === "string") {
|
|
131
|
+
const slashIdx = cfg.indexOf("/");
|
|
132
|
+
if (slashIdx !== -1) {
|
|
133
|
+
provider = cfg.slice(0, slashIdx);
|
|
134
|
+
id = cfg.slice(slashIdx + 1);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Bare model ID (e.g. "gpt-5.4") — use session provider as context
|
|
138
|
+
provider = sessionProvider;
|
|
139
|
+
id = cfg;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Object config: { model, provider?, fallbacks? }
|
|
144
|
+
if (cfg.provider) {
|
|
145
|
+
provider = cfg.provider;
|
|
146
|
+
}
|
|
147
|
+
else if (cfg.model.includes("/")) {
|
|
148
|
+
const slashIdx = cfg.model.indexOf("/");
|
|
149
|
+
provider = cfg.model.slice(0, slashIdx);
|
|
150
|
+
id = cfg.model.slice(slashIdx + 1);
|
|
151
|
+
return { provider, id };
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
provider = sessionProvider;
|
|
155
|
+
}
|
|
156
|
+
id = cfg.model;
|
|
157
|
+
}
|
|
158
|
+
if (provider && id) {
|
|
159
|
+
return { provider, id };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
90
164
|
/**
|
|
91
165
|
* Determines the next fallback model to try when the current model fails.
|
|
92
166
|
* If the current model is not in the configured list, returns the primary model.
|
|
@@ -12,13 +12,18 @@ import { validatePreferences } from "./preferences-validation.js";
|
|
|
12
12
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
13
13
|
/**
|
|
14
14
|
* Known skill directories, in priority order.
|
|
15
|
-
*
|
|
15
|
+
* Searches both the skills.sh ecosystem directory (~/.agents/skills/) and
|
|
16
|
+
* Claude Code's official directory (~/.claude/skills/). Project-level
|
|
17
|
+
* directories for both conventions are included as well.
|
|
16
18
|
* Legacy ~/.gsd/agent/skills/ is included as a fallback for pre-migration installs.
|
|
17
19
|
*/
|
|
18
20
|
export function getSkillSearchDirs(cwd) {
|
|
19
21
|
const dirs = [
|
|
20
22
|
{ dir: join(homedir(), ".agents", "skills"), method: "user-skill" },
|
|
21
23
|
{ dir: join(cwd, ".agents", "skills"), method: "project-skill" },
|
|
24
|
+
// Claude Code official skill directories
|
|
25
|
+
{ dir: join(homedir(), ".claude", "skills"), method: "user-skill" },
|
|
26
|
+
{ dir: join(cwd, ".claude", "skills"), method: "project-skill" },
|
|
22
27
|
];
|
|
23
28
|
// Legacy fallback — read skills from old GSD directory only if migration hasn't completed
|
|
24
29
|
const legacyDir = join(homedir(), ".gsd", "agent", "skills");
|
|
@@ -30,7 +30,7 @@ Ask **1–3 questions per round**. Keep each question focused on one of:
|
|
|
30
30
|
- **The biggest technical unknowns / risks** — what could fail, what hasn't been proven
|
|
31
31
|
- **What external systems/services this touches** — APIs, databases, third-party services
|
|
32
32
|
|
|
33
|
-
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` for each round. 1–3 questions per call, each as a separate question object. Keep option labels short (3–5 words). Always include a freeform "Other / let me explain" option. When the user picks that option or writes a long freeform answer, switch to plain text follow-up for that thread before resuming structured questions.
|
|
33
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` for each round. 1–3 questions per call, each as a separate question object. Keep option labels short (3–5 words). Always include a freeform "Other / let me explain" option. When the user picks that option or writes a long freeform answer, switch to plain text follow-up for that thread before resuming structured questions. **IMPORTANT: Call `ask_user_questions` exactly once per turn. Never make multiple calls with the same or overlapping questions — wait for the user's response before asking the next round.**
|
|
34
34
|
|
|
35
35
|
**If `{{structuredQuestionsAvailable}}` is `false`:** ask questions in plain text. Keep each round to 1–3 focused questions. Wait for answers before asking the next round.
|
|
36
36
|
|
|
@@ -22,7 +22,7 @@ Do **not** go deep — just enough that your questions reflect what's actually t
|
|
|
22
22
|
|
|
23
23
|
### Question rounds
|
|
24
24
|
|
|
25
|
-
Ask **1–3 questions per round** using `ask_user_questions`. Keep each question focused on one of:
|
|
25
|
+
Ask **1–3 questions per round** using `ask_user_questions`. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.** Keep each question focused on one of:
|
|
26
26
|
- **UX and user-facing behaviour** — what does the user see, click, trigger, or experience?
|
|
27
27
|
- **Edge cases and failure states** — what happens when things go wrong or are in unusual states?
|
|
28
28
|
- **Scope boundaries** — what is explicitly in vs out for this slice? What deferred to later?
|
|
@@ -887,12 +887,14 @@ export async function installPacksBatched(packs, onProgress) {
|
|
|
887
887
|
}
|
|
888
888
|
/**
|
|
889
889
|
* Check if any skills from a pack are already installed.
|
|
890
|
+
* Searches both the skills.sh ecosystem directory and Claude Code's official directory.
|
|
890
891
|
*/
|
|
891
892
|
export function isPackInstalled(pack) {
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
893
|
+
const skillsDirs = [
|
|
894
|
+
join(homedir(), ".agents", "skills"),
|
|
895
|
+
join(homedir(), ".claude", "skills"),
|
|
896
|
+
];
|
|
897
|
+
return pack.skills.every((name) => skillsDirs.some((dir) => existsSync(join(dir, name, "SKILL.md"))));
|
|
896
898
|
}
|
|
897
899
|
// ─── Init Wizard Integration ──────────────────────────────────────────────────
|
|
898
900
|
/**
|