gsd-pi 2.63.0-dev.351157b → 2.63.0-dev.786f0ff
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/cli.js +4 -0
- package/dist/headless-query.js +11 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +27 -0
- package/dist/resources/extensions/gsd/auto/phases.js +34 -0
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +32 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +79 -0
- package/dist/resources/extensions/gsd/auto-timers.js +2 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +87 -28
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +23 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +30 -2
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +7 -0
- package/dist/resources/extensions/gsd/prompts/system.md +3 -7
- package/dist/resources/extensions/gsd/safety/content-validator.js +73 -0
- package/dist/resources/extensions/gsd/safety/destructive-guard.js +34 -0
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +109 -0
- package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +83 -0
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +71 -0
- package/dist/resources/extensions/gsd/safety/git-checkpoint.js +91 -0
- package/dist/resources/extensions/gsd/safety/safety-harness.js +64 -0
- package/dist/resources/extensions/ollama/index.js +22 -10
- package/dist/resources/extensions/ollama/ollama-chat-provider.js +1 -1
- package/dist/update-cmd.js +4 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
- 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/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 +18 -18
- 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/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.js +46 -0
- package/packages/pi-coding-agent/dist/core/extensions/provider-registration.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +11 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +2 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/provider-registration.test.ts +81 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +12 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +2 -3
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +27 -0
- package/src/resources/extensions/gsd/auto/phases.ts +39 -0
- package/src/resources/extensions/gsd/auto/session.ts +5 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +36 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +88 -0
- package/src/resources/extensions/gsd/auto-timers.ts +2 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +86 -28
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +27 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +31 -2
- package/src/resources/extensions/gsd/preferences-types.ts +13 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +3 -7
- package/src/resources/extensions/gsd/safety/content-validator.ts +98 -0
- package/src/resources/extensions/gsd/safety/destructive-guard.ts +49 -0
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +151 -0
- package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +120 -0
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +108 -0
- package/src/resources/extensions/gsd/safety/git-checkpoint.ts +106 -0
- package/src/resources/extensions/gsd/safety/safety-harness.ts +105 -0
- package/src/resources/extensions/gsd/tests/complete-slice-string-coercion.test.ts +211 -0
- package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/git-checkpoint.test.ts +94 -0
- package/src/resources/extensions/gsd/tests/stuck-detection-coverage.test.ts +42 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/ollama/index.ts +20 -11
- package/src/resources/extensions/ollama/ollama-auth-mode.test.ts +20 -0
- package/src/resources/extensions/ollama/ollama-chat-provider.ts +1 -1
- package/src/resources/extensions/ollama/tests/ollama-chat-provider-stream.test.ts +82 -0
- /package/dist/web/standalone/.next/static/{QmuF-eAbuU_2MQ03t38qr → SDB1T-4NqkMjYirjjqQhr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{QmuF-eAbuU_2MQ03t38qr → SDB1T-4NqkMjYirjjqQhr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety Harness — central module for LLM damage control during auto-mode.
|
|
3
|
+
* Provides types, preference resolution, and orchestration for all safety components.
|
|
4
|
+
*
|
|
5
|
+
* Components:
|
|
6
|
+
* - evidence-collector.ts: Real-time tool call tracking
|
|
7
|
+
* - destructive-guard.ts: Bash command classification
|
|
8
|
+
* - file-change-validator.ts: Post-unit git diff vs plan
|
|
9
|
+
* - evidence-cross-ref.ts: Claimed vs actual verification evidence
|
|
10
|
+
* - git-checkpoint.ts: Pre-unit checkpoints + rollback
|
|
11
|
+
* - content-validator.ts: Output quality validation
|
|
12
|
+
*
|
|
13
|
+
* Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface SafetyHarnessConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
evidence_collection: boolean;
|
|
21
|
+
file_change_validation: boolean;
|
|
22
|
+
evidence_cross_reference: boolean;
|
|
23
|
+
destructive_command_warnings: boolean;
|
|
24
|
+
content_validation: boolean;
|
|
25
|
+
checkpoints: boolean;
|
|
26
|
+
auto_rollback: boolean;
|
|
27
|
+
timeout_scale_cap: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Defaults ───────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const DEFAULTS: SafetyHarnessConfig = {
|
|
33
|
+
enabled: true,
|
|
34
|
+
evidence_collection: true,
|
|
35
|
+
file_change_validation: true,
|
|
36
|
+
evidence_cross_reference: true,
|
|
37
|
+
destructive_command_warnings: true,
|
|
38
|
+
content_validation: true,
|
|
39
|
+
checkpoints: true,
|
|
40
|
+
auto_rollback: false,
|
|
41
|
+
timeout_scale_cap: 6,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve safety harness configuration from raw preferences.
|
|
48
|
+
* Missing fields fall back to defaults.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveSafetyHarnessConfig(
|
|
51
|
+
raw: Record<string, unknown> | undefined,
|
|
52
|
+
): SafetyHarnessConfig {
|
|
53
|
+
if (!raw) return { ...DEFAULTS };
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
enabled: typeof raw.enabled === "boolean" ? raw.enabled : DEFAULTS.enabled,
|
|
57
|
+
evidence_collection: typeof raw.evidence_collection === "boolean" ? raw.evidence_collection : DEFAULTS.evidence_collection,
|
|
58
|
+
file_change_validation: typeof raw.file_change_validation === "boolean" ? raw.file_change_validation : DEFAULTS.file_change_validation,
|
|
59
|
+
evidence_cross_reference: typeof raw.evidence_cross_reference === "boolean" ? raw.evidence_cross_reference : DEFAULTS.evidence_cross_reference,
|
|
60
|
+
destructive_command_warnings: typeof raw.destructive_command_warnings === "boolean" ? raw.destructive_command_warnings : DEFAULTS.destructive_command_warnings,
|
|
61
|
+
content_validation: typeof raw.content_validation === "boolean" ? raw.content_validation : DEFAULTS.content_validation,
|
|
62
|
+
checkpoints: typeof raw.checkpoints === "boolean" ? raw.checkpoints : DEFAULTS.checkpoints,
|
|
63
|
+
auto_rollback: typeof raw.auto_rollback === "boolean" ? raw.auto_rollback : DEFAULTS.auto_rollback,
|
|
64
|
+
timeout_scale_cap: typeof raw.timeout_scale_cap === "number" ? raw.timeout_scale_cap : DEFAULTS.timeout_scale_cap,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if the safety harness is enabled.
|
|
70
|
+
* Used as a fast gate at hook registration and phase integration points.
|
|
71
|
+
*/
|
|
72
|
+
export function isHarnessEnabled(
|
|
73
|
+
raw: Record<string, unknown> | undefined,
|
|
74
|
+
): boolean {
|
|
75
|
+
if (!raw) return DEFAULTS.enabled;
|
|
76
|
+
if (typeof raw.enabled === "boolean") return raw.enabled;
|
|
77
|
+
return DEFAULTS.enabled;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Re-exports ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
resetEvidence,
|
|
84
|
+
getEvidence,
|
|
85
|
+
getBashEvidence,
|
|
86
|
+
getFilePaths,
|
|
87
|
+
recordToolCall,
|
|
88
|
+
recordToolResult,
|
|
89
|
+
} from "./evidence-collector.js";
|
|
90
|
+
|
|
91
|
+
export type { EvidenceEntry, BashEvidence, FileWriteEvidence, FileEditEvidence } from "./evidence-collector.js";
|
|
92
|
+
|
|
93
|
+
export { classifyCommand } from "./destructive-guard.js";
|
|
94
|
+
export type { CommandClassification } from "./destructive-guard.js";
|
|
95
|
+
|
|
96
|
+
export { validateFileChanges } from "./file-change-validator.js";
|
|
97
|
+
export type { FileChangeAudit, FileViolation } from "./file-change-validator.js";
|
|
98
|
+
|
|
99
|
+
export { crossReferenceEvidence } from "./evidence-cross-ref.js";
|
|
100
|
+
export type { ClaimedEvidence, EvidenceMismatch } from "./evidence-cross-ref.js";
|
|
101
|
+
|
|
102
|
+
export { createCheckpoint, rollbackToCheckpoint, cleanupCheckpoint } from "./git-checkpoint.js";
|
|
103
|
+
|
|
104
|
+
export { validateContent } from "./content-validator.js";
|
|
105
|
+
export type { ContentViolation } from "./content-validator.js";
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// GSD Extension — String coercion regression tests for complete-slice/task tools
|
|
2
|
+
|
|
3
|
+
import { describe, test, beforeEach, afterEach } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import {
|
|
9
|
+
openDatabase,
|
|
10
|
+
closeDatabase,
|
|
11
|
+
insertMilestone,
|
|
12
|
+
insertSlice,
|
|
13
|
+
insertTask,
|
|
14
|
+
} from "../gsd-db.ts";
|
|
15
|
+
import { handleCompleteSlice } from "../tools/complete-slice.ts";
|
|
16
|
+
import type { CompleteSliceParams } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The splitPair coercion logic extracted from db-tools.ts sliceCompleteExecute.
|
|
22
|
+
* Duplicated here so we can unit-test it directly.
|
|
23
|
+
*/
|
|
24
|
+
function splitPair(s: string): [string, string] {
|
|
25
|
+
const m = s.match(/^(.+?)\s*(?:—|-)\s+(.+)$/);
|
|
26
|
+
return m ? [m[1].trim(), m[2].trim()] : [s.trim(), ""];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeValidSliceParams(): CompleteSliceParams {
|
|
30
|
+
return {
|
|
31
|
+
sliceId: "S01",
|
|
32
|
+
milestoneId: "M001",
|
|
33
|
+
sliceTitle: "Test Slice",
|
|
34
|
+
oneLiner: "Implemented test slice",
|
|
35
|
+
narrative: "Built and tested.",
|
|
36
|
+
verification: "All tests pass.",
|
|
37
|
+
deviations: "None.",
|
|
38
|
+
knownLimitations: "None.",
|
|
39
|
+
followUps: "None.",
|
|
40
|
+
keyFiles: ["src/foo.ts"],
|
|
41
|
+
keyDecisions: ["D001"],
|
|
42
|
+
patternsEstablished: [],
|
|
43
|
+
observabilitySurfaces: [],
|
|
44
|
+
provides: ["test handler"],
|
|
45
|
+
requirementsSurfaced: [],
|
|
46
|
+
drillDownPaths: [],
|
|
47
|
+
affects: [],
|
|
48
|
+
requirementsAdvanced: [{ id: "R001", how: "Handler validates" }],
|
|
49
|
+
requirementsValidated: [],
|
|
50
|
+
requirementsInvalidated: [],
|
|
51
|
+
filesModified: [{ path: "src/foo.ts", description: "Handler" }],
|
|
52
|
+
requires: [],
|
|
53
|
+
uatContent: "## Smoke Test\n\nVerify all assertions pass.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── splitPair unit tests ────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe("splitPair coercion helper (#3565)", () => {
|
|
60
|
+
test("plain string without delimiter returns string + empty", () => {
|
|
61
|
+
const [a, b] = splitPair("src/foo.ts");
|
|
62
|
+
assert.equal(a, "src/foo.ts");
|
|
63
|
+
assert.equal(b, "");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("em-dash delimiter parses both parts", () => {
|
|
67
|
+
const [id, how] = splitPair("R001 — Handler validates task completion");
|
|
68
|
+
assert.equal(id, "R001");
|
|
69
|
+
assert.equal(how, "Handler validates task completion");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("hyphen delimiter parses both parts", () => {
|
|
73
|
+
const [id, proof] = splitPair("R002 - Tests pass");
|
|
74
|
+
assert.equal(id, "R002");
|
|
75
|
+
assert.equal(proof, "Tests pass");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("string with no space around hyphen is treated as plain", () => {
|
|
79
|
+
// e.g. a file path like "src/foo-bar.ts" should not split
|
|
80
|
+
const [a, b] = splitPair("src/foo-bar.ts");
|
|
81
|
+
assert.equal(a, "src/foo-bar.ts");
|
|
82
|
+
assert.equal(b, "");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("whitespace is trimmed from both parts", () => {
|
|
86
|
+
const [id, how] = splitPair(" R003 — Trimmed value ");
|
|
87
|
+
assert.equal(id, "R003");
|
|
88
|
+
assert.equal(how, "Trimmed value");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ─── verificationEvidence sentinel tests ─────────────────────────────────
|
|
93
|
+
|
|
94
|
+
describe("verificationEvidence sentinel coercion (#3565)", () => {
|
|
95
|
+
function coerceEvidence(v: any) {
|
|
96
|
+
return typeof v === "string"
|
|
97
|
+
? { command: v, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 }
|
|
98
|
+
: v;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test("string input produces non-passing sentinel", () => {
|
|
102
|
+
const result = coerceEvidence("npm test");
|
|
103
|
+
assert.equal(result.command, "npm test");
|
|
104
|
+
assert.equal(result.exitCode, -1);
|
|
105
|
+
assert.equal(result.verdict, "unknown (coerced from string)");
|
|
106
|
+
assert.equal(result.durationMs, 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("object input passes through unchanged", () => {
|
|
110
|
+
const obj = { command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 };
|
|
111
|
+
const result = coerceEvidence(obj);
|
|
112
|
+
assert.equal(result.exitCode, 0);
|
|
113
|
+
assert.equal(result.verdict, "pass");
|
|
114
|
+
assert.equal(result.durationMs, 1234);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("sentinel exitCode is not 0 (must not fabricate success)", () => {
|
|
118
|
+
const result = coerceEvidence("anything");
|
|
119
|
+
assert.notEqual(result.exitCode, 0, "exitCode must not be 0 for coerced strings");
|
|
120
|
+
assert.ok(
|
|
121
|
+
!result.verdict.includes("pass"),
|
|
122
|
+
"verdict must not contain 'pass' for coerced strings",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── Handler integration with coerced params ─────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe("handleCompleteSlice with coerced string arrays (#3565)", () => {
|
|
130
|
+
let dbPath: string;
|
|
131
|
+
let basePath: string;
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
dbPath = path.join(
|
|
135
|
+
fs.mkdtempSync(path.join(os.tmpdir(), "gsd-coerce-")),
|
|
136
|
+
"test.db",
|
|
137
|
+
);
|
|
138
|
+
openDatabase(dbPath);
|
|
139
|
+
|
|
140
|
+
basePath = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-coerce-handler-"));
|
|
141
|
+
const sliceDir = path.join(basePath, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
|
|
142
|
+
fs.mkdirSync(sliceDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
const roadmapPath = path.join(basePath, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
roadmapPath,
|
|
147
|
+
[
|
|
148
|
+
"# M001: Test Milestone",
|
|
149
|
+
"",
|
|
150
|
+
"## Slices",
|
|
151
|
+
"",
|
|
152
|
+
'- [ ] **S01: Test Slice** `risk:medium` `depends:[]`',
|
|
153
|
+
" - After this: basic functionality works",
|
|
154
|
+
].join("\n"),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
insertMilestone({ id: "M001" });
|
|
158
|
+
insertSlice({ id: "S01", milestoneId: "M001" });
|
|
159
|
+
insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", status: "complete", title: "Task 1" });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
closeDatabase();
|
|
164
|
+
fs.rmSync(path.dirname(dbPath), { recursive: true, force: true });
|
|
165
|
+
fs.rmSync(basePath, { recursive: true, force: true });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("handler succeeds with coerced filesModified and requirementsAdvanced", async () => {
|
|
169
|
+
const params = makeValidSliceParams();
|
|
170
|
+
// Simulate coercion from plain strings
|
|
171
|
+
params.filesModified = ["src/foo.ts", "src/bar.ts"].map((f) => {
|
|
172
|
+
const [p, d] = splitPair(f);
|
|
173
|
+
return { path: p, description: d };
|
|
174
|
+
});
|
|
175
|
+
params.requirementsAdvanced = ["R001 — Handler validates task completion"].map((r) => {
|
|
176
|
+
const [id, how] = splitPair(r);
|
|
177
|
+
return { id, how };
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
181
|
+
assert.ok(!("error" in result), "handler should succeed");
|
|
182
|
+
if (!("error" in result)) {
|
|
183
|
+
const summary = fs.readFileSync(result.summaryPath, "utf-8");
|
|
184
|
+
assert.match(summary, /src\/foo\.ts/);
|
|
185
|
+
assert.match(summary, /R001/);
|
|
186
|
+
assert.match(summary, /Handler validates task completion/);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("handler succeeds with coerced requires and requirementsValidated", async () => {
|
|
191
|
+
const params = makeValidSliceParams();
|
|
192
|
+
params.requires = ["S00 — Provided base infrastructure"].map((r) => {
|
|
193
|
+
const [slice, provides] = splitPair(r);
|
|
194
|
+
return { slice, provides };
|
|
195
|
+
});
|
|
196
|
+
params.requirementsValidated = ["R002 - Tests pass"].map((r) => {
|
|
197
|
+
const [id, proof] = splitPair(r);
|
|
198
|
+
return { id, proof };
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const result = await handleCompleteSlice(params, basePath);
|
|
202
|
+
assert.ok(!("error" in result), "handler should succeed");
|
|
203
|
+
if (!("error" in result)) {
|
|
204
|
+
const summary = fs.readFileSync(result.summaryPath, "utf-8");
|
|
205
|
+
assert.match(summary, /S00/);
|
|
206
|
+
assert.match(summary, /Provided base infrastructure/);
|
|
207
|
+
assert.match(summary, /R002/);
|
|
208
|
+
assert.match(summary, /Tests pass/);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #3453: dynamic model routing must be disabled for
|
|
3
|
+
* flat-rate providers like GitHub Copilot where all models cost the same
|
|
4
|
+
* per request — routing only degrades quality with no cost benefit.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { isFlatRateProvider, resolvePreferredModelConfig } from "../auto-model-selection.ts";
|
|
10
|
+
|
|
11
|
+
describe("flat-rate provider routing guard (#3453)", () => {
|
|
12
|
+
|
|
13
|
+
test("isFlatRateProvider returns true for github-copilot", () => {
|
|
14
|
+
assert.equal(isFlatRateProvider("github-copilot"), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("isFlatRateProvider returns true for copilot alias", () => {
|
|
18
|
+
assert.equal(isFlatRateProvider("copilot"), true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("isFlatRateProvider is case-insensitive", () => {
|
|
22
|
+
assert.equal(isFlatRateProvider("GitHub-Copilot"), true);
|
|
23
|
+
assert.equal(isFlatRateProvider("GITHUB-COPILOT"), true);
|
|
24
|
+
assert.equal(isFlatRateProvider("Copilot"), true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("isFlatRateProvider returns false for anthropic", () => {
|
|
28
|
+
assert.equal(isFlatRateProvider("anthropic"), false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("isFlatRateProvider returns false for openai", () => {
|
|
32
|
+
assert.equal(isFlatRateProvider("openai"), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("resolvePreferredModelConfig returns undefined for copilot start model", () => {
|
|
36
|
+
// When the user's start model is on a flat-rate provider,
|
|
37
|
+
// resolvePreferredModelConfig should not synthesize a routing
|
|
38
|
+
// config from tier_models — it should return undefined so the
|
|
39
|
+
// user's selected model is preserved.
|
|
40
|
+
const result = resolvePreferredModelConfig("execute-task", {
|
|
41
|
+
provider: "github-copilot",
|
|
42
|
+
id: "claude-sonnet-4",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Should be undefined (no routing config created for flat-rate)
|
|
46
|
+
// Note: this only tests the guard — if explicit per-unit config exists
|
|
47
|
+
// in preferences, that takes precedence regardless.
|
|
48
|
+
assert.equal(result, undefined, "Should not create routing config for copilot");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// GSD2 — Regression tests for git-checkpoint rollback (#3576)
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { createCheckpoint, rollbackToCheckpoint, cleanupCheckpoint } from "../safety/git-checkpoint.js";
|
|
11
|
+
|
|
12
|
+
function git(args: string[], cwd: string): string {
|
|
13
|
+
return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createTempRepo(): string {
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), "ckpt-test-"));
|
|
18
|
+
git(["init"], dir);
|
|
19
|
+
git(["config", "user.email", "test@test.com"], dir);
|
|
20
|
+
git(["config", "user.name", "Test"], dir);
|
|
21
|
+
writeFileSync(join(dir, "file.txt"), "initial\n");
|
|
22
|
+
git(["add", "."], dir);
|
|
23
|
+
git(["commit", "-m", "init"], dir);
|
|
24
|
+
git(["branch", "-M", "main"], dir);
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("git-checkpoint rollback", () => {
|
|
29
|
+
it("rolls back to checkpoint on checked-out branch", (t) => {
|
|
30
|
+
const repo = createTempRepo();
|
|
31
|
+
t.after(() => rmSync(repo, { recursive: true, force: true }));
|
|
32
|
+
|
|
33
|
+
// Create checkpoint at initial commit
|
|
34
|
+
const sha = createCheckpoint(repo, "unit-1");
|
|
35
|
+
assert.ok(sha, "checkpoint should return a SHA");
|
|
36
|
+
|
|
37
|
+
// Make a second commit
|
|
38
|
+
writeFileSync(join(repo, "file.txt"), "modified\n");
|
|
39
|
+
git(["add", "."], repo);
|
|
40
|
+
git(["commit", "-m", "second"], repo);
|
|
41
|
+
|
|
42
|
+
const headBefore = git(["rev-parse", "HEAD"], repo);
|
|
43
|
+
assert.notEqual(headBefore, sha, "HEAD should have advanced");
|
|
44
|
+
|
|
45
|
+
// Rollback — this must work on the checked-out branch
|
|
46
|
+
const result = rollbackToCheckpoint(repo, "unit-1", sha);
|
|
47
|
+
assert.equal(result, true, "rollback should succeed");
|
|
48
|
+
|
|
49
|
+
const headAfter = git(["rev-parse", "HEAD"], repo);
|
|
50
|
+
assert.equal(headAfter, sha, "HEAD should match checkpoint SHA after rollback");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns false on detached HEAD", (t) => {
|
|
54
|
+
const repo = createTempRepo();
|
|
55
|
+
t.after(() => rmSync(repo, { recursive: true, force: true }));
|
|
56
|
+
|
|
57
|
+
const sha = git(["rev-parse", "HEAD"], repo);
|
|
58
|
+
git(["checkout", "--detach", sha], repo);
|
|
59
|
+
|
|
60
|
+
const result = rollbackToCheckpoint(repo, "unit-2", sha);
|
|
61
|
+
assert.equal(result, false, "rollback should fail on detached HEAD");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("cleans up checkpoint ref after rollback", (t) => {
|
|
65
|
+
const repo = createTempRepo();
|
|
66
|
+
t.after(() => rmSync(repo, { recursive: true, force: true }));
|
|
67
|
+
|
|
68
|
+
const sha = createCheckpoint(repo, "unit-3");
|
|
69
|
+
assert.ok(sha);
|
|
70
|
+
|
|
71
|
+
// Ref should exist
|
|
72
|
+
const refBefore = git(["for-each-ref", "refs/gsd/checkpoints/unit-3", "--format=%(objectname)"], repo);
|
|
73
|
+
assert.equal(refBefore, sha);
|
|
74
|
+
|
|
75
|
+
rollbackToCheckpoint(repo, "unit-3", sha);
|
|
76
|
+
|
|
77
|
+
// Ref should be cleaned up
|
|
78
|
+
const refAfter = git(["for-each-ref", "refs/gsd/checkpoints/unit-3", "--format=%(objectname)"], repo);
|
|
79
|
+
assert.equal(refAfter, "", "checkpoint ref should be removed after rollback");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("cleanupCheckpoint removes the ref without error", (t) => {
|
|
83
|
+
const repo = createTempRepo();
|
|
84
|
+
t.after(() => rmSync(repo, { recursive: true, force: true }));
|
|
85
|
+
|
|
86
|
+
const sha = createCheckpoint(repo, "unit-4");
|
|
87
|
+
assert.ok(sha);
|
|
88
|
+
|
|
89
|
+
cleanupCheckpoint(repo, "unit-4");
|
|
90
|
+
|
|
91
|
+
const ref = git(["for-each-ref", "refs/gsd/checkpoints/unit-4", "--format=%(objectname)"], repo);
|
|
92
|
+
assert.equal(ref, "", "ref should be gone");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -123,6 +123,48 @@ test("Rule 3: A-A-A-A triggers Rule 2 not Rule 3", () => {
|
|
|
123
123
|
);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
// ─── Rule 4: ENOENT same path twice in window (#3575) ───────────────────────
|
|
127
|
+
|
|
128
|
+
test("Rule 4: same ENOENT path in two entries triggers stuck", () => {
|
|
129
|
+
const result = detectStuck([
|
|
130
|
+
{ key: "A", error: "ENOENT: no such file or directory, access '/home/user/.gsd/agent/skills/debug-like-expert/SKILL.md'" },
|
|
131
|
+
{ key: "B" },
|
|
132
|
+
{ key: "A", error: "ENOENT: no such file or directory, access '/home/user/.gsd/agent/skills/debug-like-expert/SKILL.md'" },
|
|
133
|
+
]);
|
|
134
|
+
assert.notEqual(result, null);
|
|
135
|
+
assert.equal(result!.stuck, true);
|
|
136
|
+
assert.ok(result!.reason.includes("Missing file"), `reason was: ${result!.reason}`);
|
|
137
|
+
assert.ok(result!.reason.includes("ENOENT"), `reason was: ${result!.reason}`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("Rule 4: different ENOENT paths do not trigger stuck", () => {
|
|
141
|
+
const result = detectStuck([
|
|
142
|
+
{ key: "A", error: "ENOENT: no such file or directory, access '/path/a'" },
|
|
143
|
+
{ key: "B", error: "ENOENT: no such file or directory, access '/path/b'" },
|
|
144
|
+
]);
|
|
145
|
+
assert.equal(result, null);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("Rule 4: single ENOENT does not trigger stuck", () => {
|
|
149
|
+
const result = detectStuck([
|
|
150
|
+
{ key: "A", error: "ENOENT: no such file or directory, access '/path/a'" },
|
|
151
|
+
{ key: "B" },
|
|
152
|
+
]);
|
|
153
|
+
assert.equal(result, null);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("Rule 4: ENOENT paths non-consecutive still triggers", () => {
|
|
157
|
+
const result = detectStuck([
|
|
158
|
+
{ key: "A", error: "ENOENT: no such file or directory, access '/missing/skill'" },
|
|
159
|
+
{ key: "B" },
|
|
160
|
+
{ key: "C" },
|
|
161
|
+
{ key: "D", error: "ENOENT: no such file or directory, access '/missing/skill'" },
|
|
162
|
+
]);
|
|
163
|
+
assert.notEqual(result, null);
|
|
164
|
+
assert.equal(result!.stuck, true);
|
|
165
|
+
assert.ok(result!.reason.includes("/missing/skill"), `reason was: ${result!.reason}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
126
168
|
// ─── Gap documentation: 3-unit cycle evades detection ────────────────────────
|
|
127
169
|
|
|
128
170
|
test("Three-unit cycle A-B-C-A-B-C does NOT trigger stuck (documents gap L13)", () => {
|
|
@@ -48,7 +48,8 @@ export type LogComponent =
|
|
|
48
48
|
| "bootstrap" // Extension bootstrap (system-context, agent-end)
|
|
49
49
|
| "guided" // Guided flow (discuss, plan wizards)
|
|
50
50
|
| "registry" // Rule registry hook state
|
|
51
|
-
| "renderer"
|
|
51
|
+
| "renderer" // Markdown renderer and projections
|
|
52
|
+
| "safety"; // LLM safety harness
|
|
52
53
|
|
|
53
54
|
export interface LogEntry {
|
|
54
55
|
ts: string;
|
|
@@ -61,8 +61,13 @@ async function probeAndRegister(pi: ExtensionAPI): Promise<boolean> {
|
|
|
61
61
|
|
|
62
62
|
const baseUrl = client.getOllamaHost();
|
|
63
63
|
|
|
64
|
+
// Use authMode "apiKey" with a dummy key (#3440).
|
|
65
|
+
// authMode "none" requires a custom streamSimple handler, but Ollama uses
|
|
66
|
+
// the standard OpenAI-compatible streaming endpoint. Ollama ignores the
|
|
67
|
+
// Authorization header so the dummy key is harmless.
|
|
64
68
|
pi.registerProvider("ollama", {
|
|
65
|
-
authMode: "
|
|
69
|
+
authMode: "apiKey",
|
|
70
|
+
apiKey: "ollama",
|
|
66
71
|
baseUrl,
|
|
67
72
|
api: "ollama-chat",
|
|
68
73
|
streamSimple: streamOllamaChat,
|
|
@@ -100,16 +105,20 @@ export default function ollama(pi: ExtensionAPI) {
|
|
|
100
105
|
await registerOllamaTools(pi);
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
// In headless/auto mode, await the probe so the fallback resolver can
|
|
109
|
+
// see Ollama before the first LLM call (#3531 race condition).
|
|
110
|
+
// In interactive mode, keep it async for fast startup.
|
|
111
|
+
if (!ctx.hasUI) {
|
|
112
|
+
try {
|
|
113
|
+
await probeAndRegister(pi);
|
|
114
|
+
} catch { /* non-fatal */ }
|
|
115
|
+
} else {
|
|
116
|
+
probeAndRegister(pi)
|
|
117
|
+
.then((found) => {
|
|
118
|
+
if (found) ctx.ui.setStatus("ollama", "Ollama");
|
|
119
|
+
})
|
|
120
|
+
.catch(() => {});
|
|
121
|
+
}
|
|
113
122
|
});
|
|
114
123
|
|
|
115
124
|
pi.on("session_shutdown", async () => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #3440: Ollama extension must register with
|
|
3
|
+
* authMode "apiKey" (not "none") to avoid streamSimple requirement.
|
|
4
|
+
*/
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
test("Ollama registers with authMode apiKey, not none (#3440)", () => {
|
|
14
|
+
const src = readFileSync(join(__dirname, "index.ts"), "utf-8");
|
|
15
|
+
// Find the registerProvider call
|
|
16
|
+
const registerBlock = src.slice(src.indexOf("pi.registerProvider(\"ollama\""));
|
|
17
|
+
const authLine = registerBlock.match(/authMode:\s*"(\w+)"/);
|
|
18
|
+
assert.ok(authLine, "registerProvider must specify authMode");
|
|
19
|
+
assert.equal(authLine![1], "apiKey", "authMode must be apiKey, not none");
|
|
20
|
+
});
|
|
@@ -149,7 +149,7 @@ export function streamOllamaChat(
|
|
|
149
149
|
// Handle text content — process independently of tool_calls
|
|
150
150
|
// (a chunk may contain both content and tool_calls)
|
|
151
151
|
const content = chunk.message?.content ?? "";
|
|
152
|
-
if (content
|
|
152
|
+
if (content) {
|
|
153
153
|
if (thinkParser) {
|
|
154
154
|
processChunks(thinkParser.push(content));
|
|
155
155
|
} else {
|