gsd-pi 2.8.0 → 2.8.2
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/loader.js +5 -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/bash-executor.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- 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 +32 -22
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +3 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +4 -1
- 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 +7 -5
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.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/bash-executor.ts +2 -2
- 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 +35 -22
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +4 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
- package/package.json +6 -1
- 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/bash-executor.js +2 -2
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- 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 +32 -22
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +3 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- 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 +7 -5
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
- package/packages/pi-coding-agent/dist/utils/shell.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/bash-executor.ts +2 -2
- 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 +35 -22
- package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/packages/pi-coding-agent/src/index.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
- package/src/resources/extensions/bg-shell/index.ts +2 -1
- package/src/resources/extensions/browser-tools/lifecycle.ts +6 -1
- package/src/resources/extensions/gsd/auto.ts +92 -49
- package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -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 +62 -17
- package/src/resources/extensions/gsd/gitignore.ts +28 -0
- package/src/resources/extensions/gsd/guided-flow.ts +49 -11
- 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-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +27 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
- 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 +116 -39
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
- 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
|
@@ -226,7 +226,6 @@ async function main(): Promise<void> {
|
|
|
226
226
|
const milestoneId = 'M001';
|
|
227
227
|
const sliceId = 'S01';
|
|
228
228
|
const uatPath = '.gsd/milestones/M001/slices/S01/S01-UAT.md';
|
|
229
|
-
const uatResultAbsPath = '/tmp/gsd-test/S01-UAT-RESULT.md';
|
|
230
229
|
const uatResultPath = '.gsd/milestones/M001/slices/S01/S01-UAT-RESULT.md';
|
|
231
230
|
const uatType = 'artifact-driven';
|
|
232
231
|
const inlinedContext = '<!-- no context -->';
|
|
@@ -238,7 +237,6 @@ async function main(): Promise<void> {
|
|
|
238
237
|
milestoneId,
|
|
239
238
|
sliceId,
|
|
240
239
|
uatPath,
|
|
241
|
-
uatResultAbsPath,
|
|
242
240
|
uatResultPath,
|
|
243
241
|
uatType,
|
|
244
242
|
inlinedContext,
|
|
@@ -261,8 +259,8 @@ async function main(): Promise<void> {
|
|
|
261
259
|
`prompt contains sliceId value "${sliceId}" after substitution`,
|
|
262
260
|
);
|
|
263
261
|
assert(
|
|
264
|
-
promptResult?.includes(
|
|
265
|
-
`prompt contains
|
|
262
|
+
promptResult?.includes(uatResultPath) ?? false,
|
|
263
|
+
`prompt contains uatResultPath value after substitution`,
|
|
266
264
|
);
|
|
267
265
|
assert(
|
|
268
266
|
!/\{\{[^}]+\}\}/.test(promptResult ?? ''),
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|