gsd-pi 2.7.1 → 2.8.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/README.md +12 -5
- package/dist/loader.js +0 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +30 -20
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +3 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +33 -20
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +3 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/node_modules/cliui/CHANGELOG.md +121 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/node_modules/mz/HISTORY.md +66 -0
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/source-map/CHANGELOG.md +301 -0
- package/node_modules/thenify/History.md +11 -0
- package/node_modules/thenify-all/History.md +11 -0
- package/node_modules/y18n/CHANGELOG.md +100 -0
- package/node_modules/yargs/CHANGELOG.md +88 -0
- package/node_modules/yargs-parser/CHANGELOG.md +263 -0
- package/package.json +10 -2
- package/packages/pi-coding-agent/dist/config.d.ts +2 -0
- package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/config.js +4 -0
- package/packages/pi-coding-agent/dist/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +30 -20
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +3 -0
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/config.ts +5 -0
- package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/packages/pi-coding-agent/src/core/tools/bash.ts +33 -20
- package/packages/pi-coding-agent/src/index.ts +3 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/browser-tools/capture.ts +165 -0
- package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/src/resources/extensions/browser-tools/index.ts +47 -4985
- package/src/resources/extensions/browser-tools/lifecycle.ts +270 -0
- package/src/resources/extensions/browser-tools/package.json +5 -1
- package/src/resources/extensions/browser-tools/refs.ts +264 -0
- package/src/resources/extensions/browser-tools/settle.ts +197 -0
- package/src/resources/extensions/browser-tools/state.ts +408 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/src/resources/extensions/browser-tools/utils.ts +660 -0
- package/src/resources/extensions/gsd/auto.ts +74 -13
- package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
- package/src/resources/extensions/gsd/exit-command.ts +18 -0
- package/src/resources/extensions/gsd/files.ts +9 -40
- package/src/resources/extensions/gsd/git-service.ts +65 -17
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +39 -2
- package/src/resources/extensions/gsd/index.ts +111 -16
- package/src/resources/extensions/gsd/preferences.ts +8 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +25 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +64 -39
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
- package/src/resources/extensions/shared/interview-ui.ts +1 -1
- package/src/resources/extensions/ttsr/index.ts +163 -0
- package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
- package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
- package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { parseRoadmap } from "../files.ts";
|
|
2
|
+
import { parseRoadmapSlices } from "../roadmap-slices.ts";
|
|
3
|
+
|
|
4
|
+
let passed = 0;
|
|
5
|
+
let failed = 0;
|
|
6
|
+
|
|
7
|
+
function assert(condition: boolean, message: string): void {
|
|
8
|
+
if (condition) passed += 1;
|
|
9
|
+
else {
|
|
10
|
+
failed += 1;
|
|
11
|
+
console.error(`FAIL: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertEq<T>(actual: T, expected: T, message: string): void {
|
|
16
|
+
if (JSON.stringify(actual) === JSON.stringify(expected)) passed += 1;
|
|
17
|
+
else {
|
|
18
|
+
failed += 1;
|
|
19
|
+
console.error(`FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const content = `# M003: Current
|
|
24
|
+
|
|
25
|
+
**Vision:** Build the thing.
|
|
26
|
+
|
|
27
|
+
## Slices
|
|
28
|
+
- [x] **S01: First Slice** \`risk:low\` \`depends:[]\`
|
|
29
|
+
> After this: First demo works.
|
|
30
|
+
- [ ] **S02: Second Slice** \`risk:medium\` \`depends:[S01]\`
|
|
31
|
+
- [x] **S03: Third Slice** \`depends:[S01, S02]\`
|
|
32
|
+
> After this: Third demo works.
|
|
33
|
+
|
|
34
|
+
## Boundary Map
|
|
35
|
+
### S01 → S02
|
|
36
|
+
Produces:
|
|
37
|
+
foo.ts
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
console.log("\n=== parseRoadmapSlices ===");
|
|
41
|
+
const slices = parseRoadmapSlices(content);
|
|
42
|
+
assertEq(slices.length, 3, "slice count");
|
|
43
|
+
assertEq(slices[0]?.id, "S01", "first id");
|
|
44
|
+
assertEq(slices[0]?.done, true, "first done");
|
|
45
|
+
assertEq(slices[0]?.demo, "First demo works.", "first demo");
|
|
46
|
+
assertEq(slices[1]?.depends, ["S01"], "second depends");
|
|
47
|
+
assertEq(slices[1]?.risk, "medium", "second risk");
|
|
48
|
+
assertEq(slices[2]?.risk, "low", "missing risk defaults to low");
|
|
49
|
+
assertEq(slices[2]?.depends, ["S01", "S02"], "third depends");
|
|
50
|
+
|
|
51
|
+
console.log("\n=== parseRoadmap integration ===");
|
|
52
|
+
const roadmap = parseRoadmap(content);
|
|
53
|
+
assertEq(roadmap.slices, slices, "parseRoadmap uses extracted slice parser");
|
|
54
|
+
assertEq(roadmap.title, "M003: Current", "roadmap title preserved");
|
|
55
|
+
assertEq(roadmap.vision, "Build the thing.", "roadmap vision preserved");
|
|
56
|
+
assert(roadmap.boundaryMap.length === 1, "boundary map still parsed");
|
|
57
|
+
|
|
58
|
+
console.log(`Passed: ${passed}, Failed: ${failed}`);
|
|
59
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the CONTEXT.md write-gate (D031 guard chain).
|
|
3
|
+
*
|
|
4
|
+
* Exercises shouldBlockContextWrite() — a pure function that implements:
|
|
5
|
+
* (a) toolName !== "write" → pass
|
|
6
|
+
* (b) milestoneId null → pass (not in discussion)
|
|
7
|
+
* (c) path doesn't match /M\d+-CONTEXT\.md$/ → pass
|
|
8
|
+
* (d) depthVerified → pass
|
|
9
|
+
* (e) else → block with actionable reason
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import test from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { shouldBlockContextWrite } from '../index.ts';
|
|
15
|
+
|
|
16
|
+
// ─── Scenario 1: Blocks CONTEXT.md write during discussion without depth verification (absolute path) ──
|
|
17
|
+
|
|
18
|
+
test('write-gate: blocks CONTEXT.md write during discussion without depth verification (absolute path)', () => {
|
|
19
|
+
const result = shouldBlockContextWrite(
|
|
20
|
+
'write',
|
|
21
|
+
'/Users/dev/project/.gsd/milestones/M001/M001-CONTEXT.md',
|
|
22
|
+
'M001',
|
|
23
|
+
false,
|
|
24
|
+
);
|
|
25
|
+
assert.strictEqual(result.block, true, 'should block the write');
|
|
26
|
+
assert.ok(result.reason, 'should provide a reason');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ─── Scenario 2: Blocks CONTEXT.md write during discussion without depth verification (relative path) ──
|
|
30
|
+
|
|
31
|
+
test('write-gate: blocks CONTEXT.md write during discussion without depth verification (relative path)', () => {
|
|
32
|
+
const result = shouldBlockContextWrite(
|
|
33
|
+
'write',
|
|
34
|
+
'.gsd/milestones/M005/M005-CONTEXT.md',
|
|
35
|
+
'M005',
|
|
36
|
+
false,
|
|
37
|
+
);
|
|
38
|
+
assert.strictEqual(result.block, true, 'should block the write');
|
|
39
|
+
assert.ok(result.reason, 'should provide a reason');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── Scenario 3: Allows CONTEXT.md write after depth verification ──
|
|
43
|
+
|
|
44
|
+
test('write-gate: allows CONTEXT.md write after depth verification', () => {
|
|
45
|
+
const result = shouldBlockContextWrite(
|
|
46
|
+
'write',
|
|
47
|
+
'/Users/dev/project/.gsd/milestones/M001/M001-CONTEXT.md',
|
|
48
|
+
'M001',
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
assert.strictEqual(result.block, false, 'should not block after depth verification');
|
|
52
|
+
assert.strictEqual(result.reason, undefined, 'should have no reason');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ─── Scenario 4: Allows CONTEXT.md write outside discussion phase (milestoneId null) ──
|
|
56
|
+
|
|
57
|
+
test('write-gate: allows CONTEXT.md write outside discussion phase', () => {
|
|
58
|
+
const result = shouldBlockContextWrite(
|
|
59
|
+
'write',
|
|
60
|
+
'.gsd/milestones/M001/M001-CONTEXT.md',
|
|
61
|
+
null,
|
|
62
|
+
false,
|
|
63
|
+
);
|
|
64
|
+
assert.strictEqual(result.block, false, 'should not block outside discussion phase');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── Scenario 5: Allows non-CONTEXT.md writes during discussion ──
|
|
68
|
+
|
|
69
|
+
test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
|
|
70
|
+
// DISCUSSION.md
|
|
71
|
+
const r1 = shouldBlockContextWrite(
|
|
72
|
+
'write',
|
|
73
|
+
'.gsd/milestones/M001/M001-DISCUSSION.md',
|
|
74
|
+
'M001',
|
|
75
|
+
false,
|
|
76
|
+
);
|
|
77
|
+
assert.strictEqual(r1.block, false, 'DISCUSSION.md should pass');
|
|
78
|
+
|
|
79
|
+
// Slice file
|
|
80
|
+
const r2 = shouldBlockContextWrite(
|
|
81
|
+
'write',
|
|
82
|
+
'.gsd/milestones/M001/slices/S01/S01-PLAN.md',
|
|
83
|
+
'M001',
|
|
84
|
+
false,
|
|
85
|
+
);
|
|
86
|
+
assert.strictEqual(r2.block, false, 'slice plan should pass');
|
|
87
|
+
|
|
88
|
+
// Regular code file
|
|
89
|
+
const r3 = shouldBlockContextWrite(
|
|
90
|
+
'write',
|
|
91
|
+
'src/index.ts',
|
|
92
|
+
'M001',
|
|
93
|
+
false,
|
|
94
|
+
);
|
|
95
|
+
assert.strictEqual(r3.block, false, 'regular code file should pass');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── Scenario 6: Regex specificity — doesn't match S01-CONTEXT.md ──
|
|
99
|
+
|
|
100
|
+
test('write-gate: regex does not match slice context files (S01-CONTEXT.md)', () => {
|
|
101
|
+
const result = shouldBlockContextWrite(
|
|
102
|
+
'write',
|
|
103
|
+
'.gsd/milestones/M001/slices/S01/S01-CONTEXT.md',
|
|
104
|
+
'M001',
|
|
105
|
+
false,
|
|
106
|
+
);
|
|
107
|
+
assert.strictEqual(result.block, false, 'S01-CONTEXT.md should not be blocked');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ─── Scenario 7: Error message contains actionable instruction ──
|
|
111
|
+
|
|
112
|
+
test('write-gate: blocked reason contains depth_verification keyword', () => {
|
|
113
|
+
const result = shouldBlockContextWrite(
|
|
114
|
+
'write',
|
|
115
|
+
'.gsd/milestones/M999/M999-CONTEXT.md',
|
|
116
|
+
'M999',
|
|
117
|
+
false,
|
|
118
|
+
);
|
|
119
|
+
assert.strictEqual(result.block, true);
|
|
120
|
+
assert.ok(result.reason!.includes('depth_verification'), 'reason should mention depth_verification question id');
|
|
121
|
+
assert.ok(result.reason!.includes('ask_user_questions'), 'reason should mention ask_user_questions tool');
|
|
122
|
+
});
|
|
@@ -235,7 +235,7 @@ export async function showInterviewRound(
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function saveEditorToState() {
|
|
238
|
-
states[currentIdx].notes = getEditor().
|
|
238
|
+
states[currentIdx].notes = getEditor().getExpandedText().trim();
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function loadStateToEditor() {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTSR Extension — Time Traveling Stream Rules
|
|
3
|
+
*
|
|
4
|
+
* Zero-context-cost guardrails that monitor streaming output against regex
|
|
5
|
+
* patterns. On match: abort stream, inject rule as system reminder, retry.
|
|
6
|
+
* Rules cost nothing until they fire.
|
|
7
|
+
*
|
|
8
|
+
* Hooks:
|
|
9
|
+
* session_start → load rules, populate manager
|
|
10
|
+
* turn_start → reset buffers
|
|
11
|
+
* message_update → check delta against rules, abort on match
|
|
12
|
+
* turn_end → increment message count
|
|
13
|
+
* agent_end → if pending violation, inject rule via sendMessage
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
|
|
17
|
+
import type { AssistantMessageEvent } from "@gsd/pi-ai";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { join, dirname } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { TtsrManager, type Rule, type TtsrMatchContext } from "./ttsr-manager.js";
|
|
22
|
+
import { loadRules } from "./rule-loader.js";
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = dirname(__filename);
|
|
26
|
+
|
|
27
|
+
interface PendingViolation {
|
|
28
|
+
rules: Rule[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildInterruptContent(rule: Rule): string {
|
|
32
|
+
const template = readFileSync(join(__dirname, "ttsr-interrupt.md"), "utf-8");
|
|
33
|
+
return template
|
|
34
|
+
.replace("{{name}}", rule.name)
|
|
35
|
+
.replace("{{path}}", rule.path)
|
|
36
|
+
.replace("{{content}}", rule.content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract match context from an AssistantMessageEvent delta.
|
|
41
|
+
* Returns null for non-delta events.
|
|
42
|
+
*/
|
|
43
|
+
function extractDeltaContext(
|
|
44
|
+
event: AssistantMessageEvent,
|
|
45
|
+
): { delta: string; context: TtsrMatchContext } | null {
|
|
46
|
+
if (event.type === "text_delta") {
|
|
47
|
+
return {
|
|
48
|
+
delta: event.delta,
|
|
49
|
+
context: { source: "text", streamKey: "text" },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (event.type === "thinking_delta") {
|
|
53
|
+
return {
|
|
54
|
+
delta: event.delta,
|
|
55
|
+
context: { source: "thinking", streamKey: "thinking" },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (event.type === "toolcall_delta") {
|
|
59
|
+
// Extract tool name and file paths from the partial message
|
|
60
|
+
const partial = event.partial;
|
|
61
|
+
const contentBlock = partial?.content?.[event.contentIndex];
|
|
62
|
+
const toolName = contentBlock && "name" in contentBlock ? (contentBlock as any).name : undefined;
|
|
63
|
+
|
|
64
|
+
// Try to extract file paths from partial JSON arguments
|
|
65
|
+
const filePaths: string[] = [];
|
|
66
|
+
if (contentBlock && "partialJson" in contentBlock) {
|
|
67
|
+
const json = (contentBlock as any).partialJson as string | undefined;
|
|
68
|
+
if (json) {
|
|
69
|
+
// Look for file_path or path in partial JSON
|
|
70
|
+
const pathMatch = json.match(/"(?:file_path|path)"\s*:\s*"([^"]+)"/);
|
|
71
|
+
if (pathMatch) filePaths.push(pathMatch[1]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
delta: event.delta,
|
|
77
|
+
context: {
|
|
78
|
+
source: "tool",
|
|
79
|
+
toolName,
|
|
80
|
+
filePaths: filePaths.length > 0 ? filePaths : undefined,
|
|
81
|
+
streamKey: `toolcall:${event.contentIndex}`,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default function (pi: ExtensionAPI) {
|
|
89
|
+
let manager: TtsrManager | null = null;
|
|
90
|
+
let pendingViolation: PendingViolation | null = null;
|
|
91
|
+
|
|
92
|
+
// ── session_start: load rules, populate manager ─────────────────────
|
|
93
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
94
|
+
const rules = loadRules(ctx.cwd);
|
|
95
|
+
if (rules.length === 0) {
|
|
96
|
+
manager = null;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
manager = new TtsrManager();
|
|
101
|
+
let loaded = 0;
|
|
102
|
+
for (const rule of rules) {
|
|
103
|
+
if (manager.addRule(rule)) loaded++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (loaded === 0) {
|
|
107
|
+
manager = null;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── turn_start: reset buffers ───────────────────────────────────────
|
|
112
|
+
pi.on("turn_start", async () => {
|
|
113
|
+
if (!manager) return;
|
|
114
|
+
manager.resetBuffer();
|
|
115
|
+
pendingViolation = null;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── message_update: check delta against rules ───────────────────────
|
|
119
|
+
pi.on("message_update", async (event, ctx) => {
|
|
120
|
+
if (!manager || !manager.hasRules()) return;
|
|
121
|
+
if (pendingViolation) return; // Already matched, waiting for agent_end
|
|
122
|
+
|
|
123
|
+
const extracted = extractDeltaContext(event.assistantMessageEvent);
|
|
124
|
+
if (!extracted) return;
|
|
125
|
+
|
|
126
|
+
const { delta, context } = extracted;
|
|
127
|
+
const matches = manager.checkDelta(delta, context);
|
|
128
|
+
if (matches.length === 0) return;
|
|
129
|
+
|
|
130
|
+
// Match found — set pending violation and abort
|
|
131
|
+
pendingViolation = { rules: matches };
|
|
132
|
+
manager.markInjected(matches);
|
|
133
|
+
ctx.abort();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── turn_end: increment message count ───────────────────────────────
|
|
137
|
+
pi.on("turn_end", async () => {
|
|
138
|
+
if (!manager) return;
|
|
139
|
+
manager.incrementMessageCount();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── agent_end: inject violation if pending ──────────────────────────
|
|
143
|
+
pi.on("agent_end", async () => {
|
|
144
|
+
if (!manager || !pendingViolation) return;
|
|
145
|
+
|
|
146
|
+
const violation = pendingViolation;
|
|
147
|
+
pendingViolation = null;
|
|
148
|
+
|
|
149
|
+
// Build interrupt content for all matching rules
|
|
150
|
+
const interruptParts = violation.rules.map(buildInterruptContent);
|
|
151
|
+
const fullInterrupt = interruptParts.join("\n\n");
|
|
152
|
+
|
|
153
|
+
// Inject as a message that triggers a new turn
|
|
154
|
+
pi.sendMessage(
|
|
155
|
+
{
|
|
156
|
+
customType: "ttsr-violation",
|
|
157
|
+
content: fullInterrupt,
|
|
158
|
+
display: false,
|
|
159
|
+
},
|
|
160
|
+
{ triggerTurn: true },
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTSR Rule Loader
|
|
3
|
+
*
|
|
4
|
+
* Scans global (~/.gsd/agent/rules/*.md) and project-local (.gsd/rules/*.md)
|
|
5
|
+
* rule files. Parses YAML frontmatter for condition, scope, globs.
|
|
6
|
+
* Project rules override global rules with the same name.
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { join, basename } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import type { Rule } from "./ttsr-manager.js";
|
|
12
|
+
|
|
13
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
14
|
+
|
|
15
|
+
/** Minimal YAML parser for frontmatter (handles string arrays and scalars). */
|
|
16
|
+
function parseFrontmatter(raw: string): Record<string, unknown> {
|
|
17
|
+
const result: Record<string, unknown> = {};
|
|
18
|
+
let currentKey: string | null = null;
|
|
19
|
+
let currentArray: string[] | null = null;
|
|
20
|
+
|
|
21
|
+
for (const line of raw.split("\n")) {
|
|
22
|
+
const trimmed = line.trimEnd();
|
|
23
|
+
|
|
24
|
+
// Array item under current key
|
|
25
|
+
if (currentKey && /^\s+-\s+/.test(trimmed)) {
|
|
26
|
+
const value = trimmed.replace(/^\s+-\s+/, "").replace(/^["']|["']$/g, "");
|
|
27
|
+
currentArray!.push(value);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Flush previous array
|
|
32
|
+
if (currentKey && currentArray) {
|
|
33
|
+
result[currentKey] = currentArray;
|
|
34
|
+
currentKey = null;
|
|
35
|
+
currentArray = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Key-value or key-with-array
|
|
39
|
+
const kvMatch = trimmed.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
40
|
+
if (kvMatch) {
|
|
41
|
+
const [, key, value] = kvMatch;
|
|
42
|
+
if (value.length === 0) {
|
|
43
|
+
// Expect array items below
|
|
44
|
+
currentKey = key;
|
|
45
|
+
currentArray = [];
|
|
46
|
+
} else {
|
|
47
|
+
result[key] = value.replace(/^["']|["']$/g, "");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Flush trailing array
|
|
53
|
+
if (currentKey && currentArray) {
|
|
54
|
+
result[currentKey] = currentArray;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseRuleFile(filePath: string): Rule | null {
|
|
61
|
+
let content: string;
|
|
62
|
+
try {
|
|
63
|
+
content = readFileSync(filePath, "utf-8");
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
69
|
+
if (!match) return null;
|
|
70
|
+
|
|
71
|
+
const [, frontmatterRaw, body] = match;
|
|
72
|
+
const meta = parseFrontmatter(frontmatterRaw);
|
|
73
|
+
|
|
74
|
+
const condition = meta.condition;
|
|
75
|
+
if (!Array.isArray(condition) || condition.length === 0) return null;
|
|
76
|
+
|
|
77
|
+
const name = basename(filePath, ".md");
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
path: filePath,
|
|
82
|
+
content: body.trim(),
|
|
83
|
+
condition: condition as string[],
|
|
84
|
+
scope: Array.isArray(meta.scope) ? (meta.scope as string[]) : undefined,
|
|
85
|
+
globs: Array.isArray(meta.globs) ? (meta.globs as string[]) : undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scanDir(dir: string): Rule[] {
|
|
90
|
+
if (!existsSync(dir)) return [];
|
|
91
|
+
const rules: Rule[] = [];
|
|
92
|
+
try {
|
|
93
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const rule = parseRuleFile(join(dir, file));
|
|
96
|
+
if (rule) rules.push(rule);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Directory unreadable — skip
|
|
100
|
+
}
|
|
101
|
+
return rules;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load all TTSR rules from global and project-local directories.
|
|
106
|
+
* Project rules override global rules with the same name.
|
|
107
|
+
*/
|
|
108
|
+
export function loadRules(cwd: string): Rule[] {
|
|
109
|
+
const globalDir = join(homedir(), ".gsd", "agent", "rules");
|
|
110
|
+
const projectDir = join(cwd, ".gsd", "rules");
|
|
111
|
+
|
|
112
|
+
const globalRules = scanDir(globalDir);
|
|
113
|
+
const projectRules = scanDir(projectDir);
|
|
114
|
+
|
|
115
|
+
// Merge: project rules override global by name
|
|
116
|
+
const byName = new Map<string, Rule>();
|
|
117
|
+
for (const rule of globalRules) byName.set(rule.name, rule);
|
|
118
|
+
for (const rule of projectRules) byName.set(rule.name, rule);
|
|
119
|
+
|
|
120
|
+
return Array.from(byName.values());
|
|
121
|
+
}
|