@dyyz1993/pi-coding-agent 0.74.27 → 0.74.29
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/core/agent-session.d.ts +4 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +58 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/channel-registry.d.ts +2 -0
- package/dist/core/extensions/channel-registry.d.ts.map +1 -1
- package/dist/core/extensions/channel-registry.js.map +1 -1
- package/dist/core/extensions/types.d.ts +17 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/session-manager.d.ts +5 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +44 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/extensions/bash-ext/index.ts +86 -62
- package/dist/extensions/file-snapshot/index.ts +4 -1
- package/dist/extensions/hooks-engine/index.ts +104 -16
- package/dist/extensions/lsp/lsp/index.ts +21 -3
- package/dist/extensions/lsp/lsp/utils/project-scanner.ts +102 -0
- package/dist/extensions/rules-engine/index.js +64 -22
- package/dist/extensions/rules-engine/index.ts +86 -16
- package/dist/extensions/rules-engine/types.d.ts +12 -2
- package/dist/extensions/rules-engine/types.d.ts.map +1 -1
- package/dist/extensions/rules-engine/types.js.map +1 -1
- package/dist/extensions/rules-engine/types.ts +13 -2
- package/dist/extensions/session-supervisor/config.ts +3 -1
- package/dist/extensions/session-supervisor/index.ts +90 -63
- package/dist/extensions/session-supervisor/types.d.ts +321 -0
- package/dist/extensions/session-supervisor/types.d.ts.map +1 -0
- package/dist/extensions/session-supervisor/types.js +92 -0
- package/dist/extensions/session-supervisor/types.js.map +1 -0
- package/dist/extensions/session-supervisor/types.ts +8 -8
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
MatchRecord,
|
|
12
12
|
ParsedRule,
|
|
13
13
|
RuleDetail,
|
|
14
|
+
RuleMatchStatus,
|
|
14
15
|
RuleSeverity,
|
|
15
16
|
RulesChannelContract,
|
|
16
17
|
RulesChannelEvent,
|
|
@@ -54,8 +55,21 @@ export default function rulesEnginePlugin(pi: ExtensionAPI) {
|
|
|
54
55
|
let hasSentSnapshot = false;
|
|
55
56
|
let _lastCwd = "";
|
|
56
57
|
let lastMessages: unknown[] = [];
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
/** Track which rule+file combos have been injected.
|
|
60
|
+
* Forward index: ruleName → Map<filePath, toolCallId>
|
|
61
|
+
* The toolCallId is the ID of the tool call that injected this rule+file combo,
|
|
62
|
+
* used to correlate with entries_invalidated events for precise cleanup. */
|
|
63
|
+
let injectedRuleFiles: Map<string, Map<string, string>> = new Map();
|
|
64
|
+
|
|
65
|
+
/** Reverse index: toolCallId → Array<{ ruleName, filePath }>
|
|
66
|
+
* Used to quickly find and remove entries when entries_invalidated fires. */
|
|
67
|
+
let injectionByToolCallId: Map<string, Array<{ ruleName: string; filePath: string }>> = new Map();
|
|
68
|
+
|
|
69
|
+
/** Set of toolCallIds whose entries have been invalidated (deleted/folded/summarized).
|
|
70
|
+
* When checking alreadyLoaded status, if the previous toolCallId is in this set,
|
|
71
|
+
* we mark the rule as "reloaded" instead of "already_loaded". */
|
|
72
|
+
let invalidatedToolCallIds: Set<string> = new Set();
|
|
59
73
|
|
|
60
74
|
function rebuildMatchHistory(messages: unknown[]): MatchRecord[] {
|
|
61
75
|
const history: MatchRecord[] = [];
|
|
@@ -414,10 +428,21 @@ export default function rulesEnginePlugin(pi: ExtensionAPI) {
|
|
|
414
428
|
const matching = getMatchingRules(targetPath);
|
|
415
429
|
if (matching.length === 0) return undefined;
|
|
416
430
|
|
|
431
|
+
// Determine status for each rule: loaded / already_loaded / reloaded
|
|
417
432
|
const matchedRuleDetails: MatchedRuleDetail[] = matching.map((r) => {
|
|
418
433
|
const ruleName = r.name;
|
|
419
|
-
const
|
|
420
|
-
const
|
|
434
|
+
const fileMap = injectedRuleFiles.get(ruleName);
|
|
435
|
+
const isInMap = fileMap !== undefined && fileMap.has(targetPath);
|
|
436
|
+
|
|
437
|
+
let status: RuleMatchStatus;
|
|
438
|
+
if (!isInMap) {
|
|
439
|
+
status = "loaded"; // never injected before
|
|
440
|
+
} else {
|
|
441
|
+
// Was in map — check if the injection was invalidated
|
|
442
|
+
const previousCallId = fileMap.get(targetPath)!;
|
|
443
|
+
const wasInvalidated = invalidatedToolCallIds.has(previousCallId);
|
|
444
|
+
status = wasInvalidated ? "reloaded" : "already_loaded";
|
|
445
|
+
}
|
|
421
446
|
|
|
422
447
|
return {
|
|
423
448
|
name: ruleName,
|
|
@@ -427,30 +452,48 @@ export default function rulesEnginePlugin(pi: ExtensionAPI) {
|
|
|
427
452
|
(r.frontmatter.globs ?? r.frontmatter.paths)?.find((p) => matchesAnyGlob([p], targetPath)) ||
|
|
428
453
|
(r.frontmatter.globs ?? r.frontmatter.paths)?.[0] ||
|
|
429
454
|
"",
|
|
430
|
-
|
|
455
|
+
status,
|
|
456
|
+
alreadyLoaded: status === "already_loaded" || undefined,
|
|
431
457
|
};
|
|
432
458
|
});
|
|
433
459
|
|
|
434
|
-
//
|
|
435
|
-
const newRules = matching.filter((
|
|
436
|
-
const
|
|
437
|
-
return
|
|
460
|
+
// Rules that need (re-)injection: loaded or reloaded
|
|
461
|
+
const newRules = matching.filter((_r, i) => {
|
|
462
|
+
const s = matchedRuleDetails[i].status;
|
|
463
|
+
return s === "loaded" || s === "reloaded";
|
|
438
464
|
});
|
|
439
465
|
const allAlreadyLoaded = newRules.length === 0;
|
|
440
466
|
|
|
441
467
|
// Record that these rules have now been injected for this file
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
468
|
+
const toolCallId = event.toolCallId;
|
|
469
|
+
for (let i = 0; i < matching.length; i++) {
|
|
470
|
+
const ruleName = matching[i].name;
|
|
471
|
+
let fileMap = injectedRuleFiles.get(ruleName);
|
|
472
|
+
if (!fileMap) {
|
|
473
|
+
fileMap = new Map();
|
|
474
|
+
injectedRuleFiles.set(ruleName, fileMap);
|
|
475
|
+
}
|
|
476
|
+
fileMap.set(targetPath, toolCallId);
|
|
477
|
+
|
|
478
|
+
// Update reverse index
|
|
479
|
+
let reverseEntries = injectionByToolCallId.get(toolCallId);
|
|
480
|
+
if (!reverseEntries) {
|
|
481
|
+
reverseEntries = [];
|
|
482
|
+
injectionByToolCallId.set(toolCallId, reverseEntries);
|
|
483
|
+
}
|
|
484
|
+
// Avoid duplicates for same rule+file on same toolCallId
|
|
485
|
+
if (!reverseEntries.some((e) => e.ruleName === ruleName && e.filePath === targetPath)) {
|
|
486
|
+
reverseEntries.push({ ruleName, filePath: targetPath });
|
|
447
487
|
}
|
|
448
|
-
injectedFiles.add(targetPath);
|
|
449
488
|
}
|
|
450
489
|
|
|
451
490
|
const hasCritical = matching.some((r) => r.frontmatter.severity === "critical");
|
|
452
491
|
const hasHigh = matching.some((r) => r.frontmatter.severity === "high");
|
|
453
492
|
|
|
493
|
+
// Determine overall status for the payload
|
|
494
|
+
const overallStatus: RuleMatchStatus | undefined =
|
|
495
|
+
allAlreadyLoaded ? "already_loaded" : matchedRuleDetails.every((d) => d.status === "reloaded") ? "reloaded" : "loaded";
|
|
496
|
+
|
|
454
497
|
channel.emit("matched", {
|
|
455
498
|
type: "matched",
|
|
456
499
|
filePath: targetPath,
|
|
@@ -459,10 +502,11 @@ export default function rulesEnginePlugin(pi: ExtensionAPI) {
|
|
|
459
502
|
toolCallId: event.toolCallId,
|
|
460
503
|
severity: hasCritical ? "warning" : hasHigh ? "warning" : "info",
|
|
461
504
|
timestamp: Date.now(),
|
|
505
|
+
status: overallStatus,
|
|
462
506
|
alreadyLoaded: allAlreadyLoaded || undefined,
|
|
463
507
|
});
|
|
464
508
|
|
|
465
|
-
// Only inject content for
|
|
509
|
+
// Only inject content for rules that are loaded or reloaded (skip if all already loaded)
|
|
466
510
|
if (allAlreadyLoaded) {
|
|
467
511
|
return {
|
|
468
512
|
details: {
|
|
@@ -515,6 +559,8 @@ export default function rulesEnginePlugin(pi: ExtensionAPI) {
|
|
|
515
559
|
cachedMatchHash = "";
|
|
516
560
|
lastMessages = [];
|
|
517
561
|
injectedRuleFiles = new Map();
|
|
562
|
+
injectionByToolCallId = new Map();
|
|
563
|
+
invalidatedToolCallIds = new Set();
|
|
518
564
|
ctx.ui.setStatus("rules-engine", `Rules: ${rules.length} (re-injected after compact)`);
|
|
519
565
|
});
|
|
520
566
|
|
|
@@ -522,6 +568,30 @@ export default function rulesEnginePlugin(pi: ExtensionAPI) {
|
|
|
522
568
|
cachedMatchHash = "";
|
|
523
569
|
lastMessages = [];
|
|
524
570
|
injectedRuleFiles = new Map();
|
|
571
|
+
injectionByToolCallId = new Map();
|
|
572
|
+
invalidatedToolCallIds = new Set();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
pi.on("entries_invalidated", async (event) => {
|
|
576
|
+
const eventToolCallIds = event.invalidatedToolCallIds;
|
|
577
|
+
if (eventToolCallIds.length === 0) return;
|
|
578
|
+
|
|
579
|
+
// Mark affected toolCallIds as invalidated so next tool_result
|
|
580
|
+
// can distinguish "reloaded" from "already_loaded"
|
|
581
|
+
for (const callId of eventToolCallIds) {
|
|
582
|
+
if (injectionByToolCallId.has(callId)) {
|
|
583
|
+
invalidatedToolCallIds.add(callId);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Also scan forward map to find any tracked toolCallId that was invalidated
|
|
588
|
+
for (const [_ruleName, fileMap] of injectedRuleFiles) {
|
|
589
|
+
for (const [_filePath, trackedCallId] of fileMap) {
|
|
590
|
+
if (eventToolCallIds.includes(trackedCallId)) {
|
|
591
|
+
invalidatedToolCallIds.add(trackedCallId);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
525
595
|
});
|
|
526
596
|
|
|
527
597
|
pi.on("turn_end", async () => {
|
|
@@ -65,12 +65,20 @@ export interface ScannedDir {
|
|
|
65
65
|
fileCount: number;
|
|
66
66
|
ruleNames: string[];
|
|
67
67
|
}
|
|
68
|
+
export type RuleMatchStatus = "loaded" | "already_loaded" | "reloaded";
|
|
68
69
|
export interface MatchedRuleDetail {
|
|
69
70
|
name: string;
|
|
70
71
|
title: string;
|
|
71
72
|
severity: RuleSeverity;
|
|
72
73
|
matchedGlob: string;
|
|
73
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Match status:
|
|
76
|
+
* - "loaded": first time injected for this file
|
|
77
|
+
* - "already_loaded": previously injected and still in context (skipped)
|
|
78
|
+
* - "reloaded": previously injected but was invalidated (context removed), now re-injected
|
|
79
|
+
*/
|
|
80
|
+
status?: RuleMatchStatus;
|
|
81
|
+
/** @deprecated Use status instead. True when this rule was already injected for the same file. */
|
|
74
82
|
alreadyLoaded?: boolean;
|
|
75
83
|
}
|
|
76
84
|
export interface MatchRecord {
|
|
@@ -117,7 +125,9 @@ export interface MatchedPayload {
|
|
|
117
125
|
toolCallId: string;
|
|
118
126
|
severity: "info" | "warning";
|
|
119
127
|
timestamp: number;
|
|
120
|
-
/**
|
|
128
|
+
/** Overall match status when all rules share the same state */
|
|
129
|
+
status?: RuleMatchStatus;
|
|
130
|
+
/** @deprecated Use status instead */
|
|
121
131
|
alreadyLoaded?: boolean;
|
|
122
132
|
}
|
|
123
133
|
export interface InjectedPayload {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAE3E,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,eAAe;IAC/B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,eAAe,CAAC;IAC7B,eAAe,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,aAAa,EAAE,UAAU,EAAE,CAAC;IAC5B,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;CACF;AAED,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,YAAY,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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","sourcesContent":["export type RuleSeverity = \"critical\" | \"high\" | \"medium\" | \"low\" | \"hint\";\n\nexport type RuleScope = \"user\" | \"pi\" | \"project\" | \"managed\";\n\nexport interface RuleFrontmatter {\n\tglobs?: string[];\n\tpaths?: string[];\n\tdescription?: string;\n\tseverity?: RuleSeverity;\n\tallowedTools?: string[];\n\twhenToUse?: string;\n\tversion?: string;\n\tmodel?: string;\n\tskills?: string;\n\teffort?: string;\n\tuserInvocable?: string;\n\tcontext?: \"inline\" | \"fork\";\n\tagent?: string;\n\tshell?: string;\n\tnotifyOnMatch?: boolean;\n\tskipInPrompt?: boolean;\n}\n\nexport interface ParsedRule {\n\tname: string;\n\tfilePath: string;\n\ttitle: string;\n\tcontent: string;\n\tscope: RuleScope;\n\tsource: string;\n\tfrontmatter: RuleFrontmatter;\n\tisUnconditional: boolean;\n}\n\nexport interface RuleCache {\n\trules: ParsedRule[];\n\tunconditional: ParsedRule[];\n\tconditional: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface CachedRules {\n\trules: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface RulesConfig {\n\tcacheTTL: number;\n\tnotifyOnLoad: boolean;\n\tnotifyOnMatch: boolean;\n\tdirs?: {\n\t\tuser?: string[];\n\t\tpi?: string[];\n\t\tproject?: string[];\n\t\tmanaged?: string[];\n\t};\n}\n\nexport interface RuleDetail {\n\tname: string;\n\ttitle: string;\n\tfilePath: string;\n\tscope: RuleScope;\n\tsource: string;\n\tseverity: RuleSeverity;\n\tisUnconditional: boolean;\n\tglobs: string[];\n\tdescription?: string;\n}\n\nexport interface ScannedDir {\n\tdir: string;\n\tfileCount: number;\n\truleNames: string[];\n}\n\nexport type RuleMatchStatus = \"loaded\" | \"already_loaded\" | \"reloaded\";\n\nexport interface MatchedRuleDetail {\n\tname: string;\n\ttitle: string;\n\tseverity: RuleSeverity;\n\tmatchedGlob: string;\n\t/**\n\t * Match status:\n\t * - \"loaded\": first time injected for this file\n\t * - \"already_loaded\": previously injected and still in context (skipped)\n\t * - \"reloaded\": previously injected but was invalidated (context removed), now re-injected\n\t */\n\tstatus?: RuleMatchStatus;\n\t/** @deprecated Use status instead. True when this rule was already injected for the same file. */\n\talreadyLoaded?: boolean;\n}\n\nexport interface MatchRecord {\n\tfilePath: string;\n\truleNames: string[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n\tmatchedRuleDetails?: MatchedRuleDetail[];\n}\n\nexport interface LifecycleEntry {\n\tevent: \"loaded\" | \"restored\" | \"injected\" | \"reloaded\" | \"unloaded\" | \"expired\";\n\tmessage: string;\n\truleCount?: number;\n\ttimestamp: number;\n\tdetails?: {\n\t\tscannedDirs?: ScannedDir[];\n\t\tconfigSource?: string;\n\t\tcacheHit?: boolean;\n\t\tinjectedRules?: Array<{ name: string; promptDelta: number }>;\n\t};\n}\n\nexport interface SnapshotPayload {\n\ttype: \"snapshot\";\n\trules: RuleDetail[];\n\tinjectedRuleNames: string[];\n\ttotalRules: number;\n\tunconditionalCount: number;\n\tconditionalCount: number;\n\tmatchHistory: MatchRecord[];\n\tlifecycleLog: LifecycleEntry[];\n\tloadedAt: number;\n\tcacheTTL: number;\n}\n\nexport interface MatchedPayload {\n\ttype: \"matched\";\n\tfilePath: string;\n\tmatchedRules: MatchedRuleDetail[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n\t/** Overall match status when all rules share the same state */\n\tstatus?: RuleMatchStatus;\n\t/** @deprecated Use status instead */\n\talreadyLoaded?: boolean;\n}\n\nexport interface InjectedPayload {\n\ttype: \"injected\";\n\truleNames: string[];\n\tsystemPromptLength: number;\n}\n\nexport interface ReloadedPayload {\n\ttype: \"reloaded\";\n\trules: RuleDetail[];\n\tloadedAt: number;\n}\n\nexport interface UnloadedPayload {\n\ttype: \"unloaded\";\n\treason: string;\n}\n\nexport type RulesChannelEvent = SnapshotPayload | MatchedPayload | InjectedPayload | ReloadedPayload | UnloadedPayload;\n\nexport const RULES_CHANNEL_NAME = \"rules-engine\";\n\nexport interface RulesChannelContract {\n\tmethods: {\n\t\tgetSnapshot: {\n\t\t\tparams: { cwd?: string };\n\t\t\treturn: SnapshotPayload;\n\t\t};\n\t};\n\tevents: {\n\t\tsnapshot: SnapshotPayload;\n\t\tmatched: MatchedPayload;\n\t\tinjected: InjectedPayload;\n\t\treloaded: ReloadedPayload;\n\t\tunloaded: UnloadedPayload;\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAmKA,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC","sourcesContent":["export type RuleSeverity = \"critical\" | \"high\" | \"medium\" | \"low\" | \"hint\";\n\nexport type RuleScope = \"user\" | \"pi\" | \"project\" | \"managed\";\n\nexport interface RuleFrontmatter {\n\tglobs?: string[];\n\tpaths?: string[];\n\tdescription?: string;\n\tseverity?: RuleSeverity;\n\tallowedTools?: string[];\n\twhenToUse?: string;\n\tversion?: string;\n\tmodel?: string;\n\tskills?: string;\n\teffort?: string;\n\tuserInvocable?: string;\n\tcontext?: \"inline\" | \"fork\";\n\tagent?: string;\n\tshell?: string;\n\tnotifyOnMatch?: boolean;\n\tskipInPrompt?: boolean;\n}\n\nexport interface ParsedRule {\n\tname: string;\n\tfilePath: string;\n\ttitle: string;\n\tcontent: string;\n\tscope: RuleScope;\n\tsource: string;\n\tfrontmatter: RuleFrontmatter;\n\tisUnconditional: boolean;\n}\n\nexport interface RuleCache {\n\trules: ParsedRule[];\n\tunconditional: ParsedRule[];\n\tconditional: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface CachedRules {\n\trules: ParsedRule[];\n\tloadedAt: number;\n}\n\nexport interface RulesConfig {\n\tcacheTTL: number;\n\tnotifyOnLoad: boolean;\n\tnotifyOnMatch: boolean;\n\tdirs?: {\n\t\tuser?: string[];\n\t\tpi?: string[];\n\t\tproject?: string[];\n\t\tmanaged?: string[];\n\t};\n}\n\nexport interface RuleDetail {\n\tname: string;\n\ttitle: string;\n\tfilePath: string;\n\tscope: RuleScope;\n\tsource: string;\n\tseverity: RuleSeverity;\n\tisUnconditional: boolean;\n\tglobs: string[];\n\tdescription?: string;\n}\n\nexport interface ScannedDir {\n\tdir: string;\n\tfileCount: number;\n\truleNames: string[];\n}\n\nexport type RuleMatchStatus = \"loaded\" | \"already_loaded\" | \"reloaded\";\n\nexport interface MatchedRuleDetail {\n\tname: string;\n\ttitle: string;\n\tseverity: RuleSeverity;\n\tmatchedGlob: string;\n\t/**\n\t * Match status:\n\t * - \"loaded\": first time injected for this file\n\t * - \"already_loaded\": previously injected and still in context (skipped)\n\t * - \"reloaded\": previously injected but was invalidated (context removed), now re-injected\n\t */\n\tstatus?: RuleMatchStatus;\n\t/** @deprecated Use status instead. True when this rule was already injected for the same file. */\n\talreadyLoaded?: boolean;\n}\n\nexport interface MatchRecord {\n\tfilePath: string;\n\truleNames: string[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n\tmatchedRuleDetails?: MatchedRuleDetail[];\n}\n\nexport interface LifecycleEntry {\n\tevent: \"loaded\" | \"restored\" | \"injected\" | \"reloaded\" | \"unloaded\" | \"expired\";\n\tmessage: string;\n\truleCount?: number;\n\ttimestamp: number;\n\tdetails?: {\n\t\tscannedDirs?: ScannedDir[];\n\t\tconfigSource?: string;\n\t\tcacheHit?: boolean;\n\t\tinjectedRules?: Array<{ name: string; promptDelta: number }>;\n\t};\n}\n\nexport interface SnapshotPayload {\n\ttype: \"snapshot\";\n\trules: RuleDetail[];\n\tinjectedRuleNames: string[];\n\ttotalRules: number;\n\tunconditionalCount: number;\n\tconditionalCount: number;\n\tmatchHistory: MatchRecord[];\n\tlifecycleLog: LifecycleEntry[];\n\tloadedAt: number;\n\tcacheTTL: number;\n}\n\nexport interface MatchedPayload {\n\ttype: \"matched\";\n\tfilePath: string;\n\tmatchedRules: MatchedRuleDetail[];\n\ttoolName: string;\n\ttoolCallId: string;\n\tseverity: \"info\" | \"warning\";\n\ttimestamp: number;\n\t/** Overall match status when all rules share the same state */\n\tstatus?: RuleMatchStatus;\n\t/** @deprecated Use status instead */\n\talreadyLoaded?: boolean;\n}\n\nexport interface InjectedPayload {\n\ttype: \"injected\";\n\truleNames: string[];\n\tsystemPromptLength: number;\n}\n\nexport interface ReloadedPayload {\n\ttype: \"reloaded\";\n\trules: RuleDetail[];\n\tloadedAt: number;\n}\n\nexport interface UnloadedPayload {\n\ttype: \"unloaded\";\n\treason: string;\n}\n\nexport type RulesChannelEvent = SnapshotPayload | MatchedPayload | InjectedPayload | ReloadedPayload | UnloadedPayload;\n\nexport const RULES_CHANNEL_NAME = \"rules-engine\";\n\nexport interface RulesChannelContract {\n\tmethods: {\n\t\tgetSnapshot: {\n\t\t\tparams: { cwd?: string };\n\t\t\treturn: SnapshotPayload;\n\t\t};\n\t};\n\tevents: {\n\t\tsnapshot: SnapshotPayload;\n\t\tmatched: MatchedPayload;\n\t\tinjected: InjectedPayload;\n\t\treloaded: ReloadedPayload;\n\t\tunloaded: UnloadedPayload;\n\t};\n}\n"]}
|
|
@@ -74,12 +74,21 @@ export interface ScannedDir {
|
|
|
74
74
|
ruleNames: string[];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export type RuleMatchStatus = "loaded" | "already_loaded" | "reloaded";
|
|
78
|
+
|
|
77
79
|
export interface MatchedRuleDetail {
|
|
78
80
|
name: string;
|
|
79
81
|
title: string;
|
|
80
82
|
severity: RuleSeverity;
|
|
81
83
|
matchedGlob: string;
|
|
82
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Match status:
|
|
86
|
+
* - "loaded": first time injected for this file
|
|
87
|
+
* - "already_loaded": previously injected and still in context (skipped)
|
|
88
|
+
* - "reloaded": previously injected but was invalidated (context removed), now re-injected
|
|
89
|
+
*/
|
|
90
|
+
status?: RuleMatchStatus;
|
|
91
|
+
/** @deprecated Use status instead. True when this rule was already injected for the same file. */
|
|
83
92
|
alreadyLoaded?: boolean;
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -127,7 +136,9 @@ export interface MatchedPayload {
|
|
|
127
136
|
toolCallId: string;
|
|
128
137
|
severity: "info" | "warning";
|
|
129
138
|
timestamp: number;
|
|
130
|
-
/**
|
|
139
|
+
/** Overall match status when all rules share the same state */
|
|
140
|
+
status?: RuleMatchStatus;
|
|
141
|
+
/** @deprecated Use status instead */
|
|
131
142
|
alreadyLoaded?: boolean;
|
|
132
143
|
}
|
|
133
144
|
|
|
@@ -15,7 +15,9 @@ const DEFAULT_CONFIG: SupervisorConfig = {
|
|
|
15
15
|
maxContinueCount: 5,
|
|
16
16
|
defaultDelayMs: 30_000,
|
|
17
17
|
pauseThresholdMs: 300_000,
|
|
18
|
-
guards: [
|
|
18
|
+
guards: [
|
|
19
|
+
{ name: "incomplete-keywords", type: "keyword", enable: true, keywords: ["TODO", "FIXME", "WIP", "HACK"] },
|
|
20
|
+
],
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export function loadConfig(sessionDataDir: string, projectDataDir: string): SupervisorConfig {
|
|
@@ -36,6 +36,10 @@ function log(msg: string) {
|
|
|
36
36
|
appendFileSync(LOG_FILE, line);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const DEFAULT_GUARDS: GuardConfig[] = [
|
|
40
|
+
{ name: "incomplete-keywords", type: "keyword", enable: true, keywords: ["TODO", "FIXME", "WIP", "HACK"] },
|
|
41
|
+
];
|
|
42
|
+
|
|
39
43
|
export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
40
44
|
let config: SupervisorConfig;
|
|
41
45
|
let enabled = false;
|
|
@@ -72,8 +76,8 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
72
76
|
const { server: channel } =
|
|
73
77
|
createTypedChannel<SupervisorChannelContract>(rawChannel);
|
|
74
78
|
|
|
75
|
-
channel.handle("
|
|
76
|
-
channel.handle("
|
|
79
|
+
channel.handle("getStatus", async () => getStatus());
|
|
80
|
+
channel.handle("requestPause", async (params) => {
|
|
77
81
|
const delayMs = params.delayMs ?? config.defaultDelayMs;
|
|
78
82
|
const result = schedulerInstance.scheduleContinue(
|
|
79
83
|
"manual-pause",
|
|
@@ -90,7 +94,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
90
94
|
return result;
|
|
91
95
|
});
|
|
92
96
|
|
|
93
|
-
channel.handle("
|
|
97
|
+
channel.handle("cancelPause", async () => {
|
|
94
98
|
const cancelled = schedulerInstance.cancelTimer("manual-pause");
|
|
95
99
|
if (cancelled) {
|
|
96
100
|
channel.emit("supervisor.pauseCancelled", { reason: "Cancelled via channel" });
|
|
@@ -98,7 +102,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
98
102
|
return { cancelled };
|
|
99
103
|
});
|
|
100
104
|
|
|
101
|
-
channel.handle("
|
|
105
|
+
channel.handle("forceContinue", async (params) => {
|
|
102
106
|
schedulerInstance.cancelAll();
|
|
103
107
|
currentState = "continuing";
|
|
104
108
|
emitStatusChanged();
|
|
@@ -106,7 +110,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
106
110
|
return { triggered: true };
|
|
107
111
|
});
|
|
108
112
|
|
|
109
|
-
channel.handle("
|
|
113
|
+
channel.handle("disable", async () => {
|
|
110
114
|
enabled = false;
|
|
111
115
|
schedulerInstance.cancelAll();
|
|
112
116
|
currentState = "disabled";
|
|
@@ -114,16 +118,16 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
114
118
|
return { disabled: true };
|
|
115
119
|
});
|
|
116
120
|
|
|
117
|
-
channel.handle("
|
|
121
|
+
channel.handle("enable", async () => {
|
|
118
122
|
enabled = true;
|
|
119
123
|
currentState = "idle";
|
|
120
124
|
emitStatusChanged();
|
|
121
125
|
return { enabled: true };
|
|
122
126
|
});
|
|
123
127
|
|
|
124
|
-
channel.handle("
|
|
128
|
+
channel.handle("getTaskReport", async () => ({ tasks: lastTaskReports }));
|
|
125
129
|
|
|
126
|
-
channel.handle("
|
|
130
|
+
channel.handle("checkToolStatus", async (params) => {
|
|
127
131
|
const targetChannelName = params.channelName ?? params.toolName;
|
|
128
132
|
try {
|
|
129
133
|
const result = await rawChannel.call(
|
|
@@ -222,8 +226,8 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
222
226
|
config.smallModel = modelFlag;
|
|
223
227
|
}
|
|
224
228
|
|
|
225
|
-
//
|
|
226
|
-
projectRoot = ctx.
|
|
229
|
+
// projectRoot is the git root (worktree-aware), correct for specs file resolution
|
|
230
|
+
projectRoot = ctx.projectRoot ?? ctx.cwd;
|
|
227
231
|
|
|
228
232
|
schedulerInstance = new Scheduler(
|
|
229
233
|
config.maxContinueCount,
|
|
@@ -276,7 +280,29 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
276
280
|
log(`guard[${guard.name}] completed=${result.completed}, remaining=${result.remainingItems.length}`);
|
|
277
281
|
}
|
|
278
282
|
|
|
279
|
-
|
|
283
|
+
lastTaskReports = reports;
|
|
284
|
+
channel.emit("supervisor.taskReport", { tasks: reports });
|
|
285
|
+
|
|
286
|
+
// Phase 2: If any guard says incomplete → continue immediately
|
|
287
|
+
const hasIncompleteGuards = guardResults.some((r) => !r.completed && r.remainingItems.length > 0);
|
|
288
|
+
|
|
289
|
+
if (hasIncompleteGuards) {
|
|
290
|
+
log(`Guards detected incomplete tasks`);
|
|
291
|
+
specsIterationCount++;
|
|
292
|
+
|
|
293
|
+
const continueMessage = generateContinueMessage(
|
|
294
|
+
activeGuards,
|
|
295
|
+
guardResults,
|
|
296
|
+
null,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
lastCheckResult = { completed: false, confidence: 0.9, incompleteTasks: [], guardResults };
|
|
300
|
+
|
|
301
|
+
scheduleContinue(continueMessage);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Phase 3: All guards passed → run fallback model check
|
|
280
306
|
const modelCheck = await checkWithSmallModel(
|
|
281
307
|
event.messages as Array<{ role: string; content: unknown }>,
|
|
282
308
|
config,
|
|
@@ -284,14 +310,9 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
284
310
|
ctx.sessionSignal,
|
|
285
311
|
);
|
|
286
312
|
|
|
287
|
-
lastTaskReports = reports;
|
|
288
|
-
channel.emit("supervisor.taskReport", { tasks: reports });
|
|
289
|
-
|
|
290
|
-
// Phase 2: Determine if we should continue
|
|
291
|
-
const hasIncompleteGuards = guardResults.some((r) => !r.completed && r.remainingItems.length > 0);
|
|
292
313
|
const hasModelIncomplete = modelCheck.completed === false || modelCheck.incompleteTasks.length > 0;
|
|
293
314
|
|
|
294
|
-
if (!
|
|
315
|
+
if (!hasModelIncomplete) {
|
|
295
316
|
log(`All guards passed + model check passed → idle`);
|
|
296
317
|
currentState = "idle";
|
|
297
318
|
lastCheckResult = { ...modelCheck, guardResults };
|
|
@@ -299,10 +320,8 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
299
320
|
return;
|
|
300
321
|
}
|
|
301
322
|
|
|
302
|
-
// Phase
|
|
303
|
-
log(`
|
|
304
|
-
specsIterationCount++;
|
|
305
|
-
|
|
323
|
+
// Phase 4: Model detected incompleteness → continue with model's assessment
|
|
324
|
+
log(`Model detected incomplete tasks`);
|
|
306
325
|
const continueMessage = generateContinueMessage(
|
|
307
326
|
activeGuards,
|
|
308
327
|
guardResults,
|
|
@@ -310,41 +329,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
310
329
|
);
|
|
311
330
|
|
|
312
331
|
lastCheckResult = { ...modelCheck, guardResults };
|
|
313
|
-
|
|
314
|
-
// Phase 4: Schedule continue
|
|
315
|
-
const delayMs = config.defaultDelayMs;
|
|
316
|
-
|
|
317
|
-
if (schedulerInstance.shouldPause(delayMs)) {
|
|
318
|
-
currentState = "paused";
|
|
319
|
-
emitStatusChanged();
|
|
320
|
-
channel.emit("supervisor.pauseRequested", {
|
|
321
|
-
delayMs,
|
|
322
|
-
reason: continueMessage.slice(0, 200),
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
pi.background(async (signal) => {
|
|
327
|
-
await new Promise<void>((resolve) => {
|
|
328
|
-
const timer = setTimeout(resolve, delayMs);
|
|
329
|
-
signal.addEventListener("abort", () => {
|
|
330
|
-
clearTimeout(timer);
|
|
331
|
-
resolve();
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
if (signal.aborted) return;
|
|
336
|
-
|
|
337
|
-
currentState = "continuing";
|
|
338
|
-
emitStatusChanged();
|
|
339
|
-
pi.sendMessage(
|
|
340
|
-
{
|
|
341
|
-
customType: "supervisor_continue",
|
|
342
|
-
content: continueMessage,
|
|
343
|
-
display: true,
|
|
344
|
-
},
|
|
345
|
-
{ triggerTurn: true },
|
|
346
|
-
);
|
|
347
|
-
});
|
|
332
|
+
scheduleContinue(continueMessage);
|
|
348
333
|
} catch (err) {
|
|
349
334
|
log(`agent_end error: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
335
|
currentState = "idle";
|
|
@@ -359,8 +344,46 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
359
344
|
|
|
360
345
|
// ── Guard Check Functions ──
|
|
361
346
|
|
|
347
|
+
function scheduleContinue(continueMessage: string): void {
|
|
348
|
+
const delayMs = config.defaultDelayMs;
|
|
349
|
+
|
|
350
|
+
if (schedulerInstance.shouldPause(delayMs)) {
|
|
351
|
+
currentState = "paused";
|
|
352
|
+
emitStatusChanged();
|
|
353
|
+
channel.emit("supervisor.pauseRequested", {
|
|
354
|
+
delayMs,
|
|
355
|
+
reason: continueMessage.slice(0, 200),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
pi.background(async (signal) => {
|
|
360
|
+
await new Promise<void>((resolve) => {
|
|
361
|
+
const timer = setTimeout(resolve, delayMs);
|
|
362
|
+
signal.addEventListener("abort", () => {
|
|
363
|
+
clearTimeout(timer);
|
|
364
|
+
resolve();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (signal.aborted) return;
|
|
369
|
+
|
|
370
|
+
currentState = "continuing";
|
|
371
|
+
emitStatusChanged();
|
|
372
|
+
pi.sendMessage(
|
|
373
|
+
{
|
|
374
|
+
customType: "supervisor_continue",
|
|
375
|
+
content: continueMessage,
|
|
376
|
+
display: true,
|
|
377
|
+
},
|
|
378
|
+
{ triggerTurn: true },
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
362
383
|
function getActiveGuards(): GuardConfig[] {
|
|
363
|
-
|
|
384
|
+
const guards = config.guards ?? [];
|
|
385
|
+
const source = guards.length > 0 ? guards : DEFAULT_GUARDS;
|
|
386
|
+
return source.filter((g) => g.enable !== false);
|
|
364
387
|
}
|
|
365
388
|
|
|
366
389
|
async function runGuardCheck(
|
|
@@ -599,7 +622,7 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
599
622
|
function generateContinueMessage(
|
|
600
623
|
guards: GuardConfig[],
|
|
601
624
|
results: GuardCheckResult[],
|
|
602
|
-
modelCheck: CheckResult,
|
|
625
|
+
modelCheck: CheckResult | null,
|
|
603
626
|
): string {
|
|
604
627
|
// Priority: first incomplete guard generates the message
|
|
605
628
|
for (let i = 0; i < guards.length; i++) {
|
|
@@ -640,11 +663,15 @@ export default function sessionSupervisorExtension(pi: ExtensionAPI) {
|
|
|
640
663
|
}
|
|
641
664
|
|
|
642
665
|
// Fallback: generic continue from model check
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
666
|
+
if (modelCheck) {
|
|
667
|
+
const tasks = modelCheck.incompleteTasks.map((t) => `[${t.severity}] ${t.description}`);
|
|
668
|
+
return CONTINUE_PROMPT(
|
|
669
|
+
modelCheck.modelResponse ?? "Model detected incomplete tasks",
|
|
670
|
+
tasks.length > 0 ? tasks : ["Continue working"],
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return CONTINUE_PROMPT("Incomplete tasks detected", ["Please continue working on remaining items."]);
|
|
648
675
|
}
|
|
649
676
|
|
|
650
677
|
function generateBlockMessage(
|