@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.
Files changed (36) hide show
  1. package/dist/core/agent-session.d.ts +4 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +58 -0
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/extensions/channel-registry.d.ts +2 -0
  6. package/dist/core/extensions/channel-registry.d.ts.map +1 -1
  7. package/dist/core/extensions/channel-registry.js.map +1 -1
  8. package/dist/core/extensions/types.d.ts +17 -1
  9. package/dist/core/extensions/types.d.ts.map +1 -1
  10. package/dist/core/extensions/types.js.map +1 -1
  11. package/dist/core/session-manager.d.ts +5 -0
  12. package/dist/core/session-manager.d.ts.map +1 -1
  13. package/dist/core/session-manager.js +44 -1
  14. package/dist/core/session-manager.js.map +1 -1
  15. package/dist/extensions/bash-ext/index.ts +86 -62
  16. package/dist/extensions/file-snapshot/index.ts +4 -1
  17. package/dist/extensions/hooks-engine/index.ts +104 -16
  18. package/dist/extensions/lsp/lsp/index.ts +21 -3
  19. package/dist/extensions/lsp/lsp/utils/project-scanner.ts +102 -0
  20. package/dist/extensions/rules-engine/index.js +64 -22
  21. package/dist/extensions/rules-engine/index.ts +86 -16
  22. package/dist/extensions/rules-engine/types.d.ts +12 -2
  23. package/dist/extensions/rules-engine/types.d.ts.map +1 -1
  24. package/dist/extensions/rules-engine/types.js.map +1 -1
  25. package/dist/extensions/rules-engine/types.ts +13 -2
  26. package/dist/extensions/session-supervisor/config.ts +3 -1
  27. package/dist/extensions/session-supervisor/index.ts +90 -63
  28. package/dist/extensions/session-supervisor/types.d.ts +321 -0
  29. package/dist/extensions/session-supervisor/types.d.ts.map +1 -0
  30. package/dist/extensions/session-supervisor/types.js +92 -0
  31. package/dist/extensions/session-supervisor/types.js.map +1 -0
  32. package/dist/extensions/session-supervisor/types.ts +8 -8
  33. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  34. package/dist/modes/rpc/rpc-mode.js +1 -1
  35. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  36. 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
- /** Track which rule+file combos have been injected (ruleName -> Set<filePath>) */
58
- let injectedRuleFiles: Map<string, Set<string>> = new Map();
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 injectedFiles = injectedRuleFiles.get(ruleName);
420
- const wasAlreadyLoaded = injectedFiles !== undefined && injectedFiles.has(targetPath);
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
- alreadyLoaded: wasAlreadyLoaded || undefined,
455
+ status,
456
+ alreadyLoaded: status === "already_loaded" || undefined,
431
457
  };
432
458
  });
433
459
 
434
- // Determine which rules are newly injected vs already loaded
435
- const newRules = matching.filter((r) => {
436
- const injectedFiles = injectedRuleFiles.get(r.name);
437
- return !injectedFiles || !injectedFiles.has(targetPath);
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
- for (const r of matching) {
443
- let injectedFiles = injectedRuleFiles.get(r.name);
444
- if (!injectedFiles) {
445
- injectedFiles = new Set();
446
- injectedRuleFiles.set(r.name, injectedFiles);
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 NEW rules (skip if all already loaded)
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
- /** True when this rule was already injected for the same file in a previous tool call */
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
- /** True when all matched rules were already injected in a previous tool call */
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":"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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 interface MatchedRuleDetail {\n\tname: string;\n\ttitle: string;\n\tseverity: RuleSeverity;\n\tmatchedGlob: string;\n\t/** True when this rule was already injected for the same file in a previous tool call */\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/** True when all matched rules were already injected in a previous tool call */\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
+ {"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":"AAwJA,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 interface MatchedRuleDetail {\n\tname: string;\n\ttitle: string;\n\tseverity: RuleSeverity;\n\tmatchedGlob: string;\n\t/** True when this rule was already injected for the same file in a previous tool call */\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/** True when all matched rules were already injected in a previous tool call */\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
+ {"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
- /** True when this rule was already injected for the same file in a previous tool call */
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
- /** True when all matched rules were already injected in a previous tool call */
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("supervisor.getStatus", async () => getStatus());
76
- channel.handle("supervisor.requestPause", async (params) => {
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("supervisor.cancelPause", async () => {
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("supervisor.forceContinue", async (params) => {
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("supervisor.disable", async () => {
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("supervisor.enable", async () => {
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("supervisor.getTaskReport", async () => ({ tasks: lastTaskReports }));
128
+ channel.handle("getTaskReport", async () => ({ tasks: lastTaskReports }));
125
129
 
126
- channel.handle("supervisor.checkToolStatus", async (params) => {
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
- // Determine project root for specs file resolution
226
- projectRoot = ctx.projectDataDir ?? process.cwd();
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
- // Also run the generic model-based check as fallback (if no custom guards or as additional check)
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 (!hasIncompleteGuards && !hasModelIncomplete) {
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 3: Generate continue message from first incomplete guard
303
- log(`Incomplete tasks detected, scheduling continue...`);
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
- return (config.guards ?? []).filter((g) => g.enable !== false);
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
- const tasks = modelCheck.incompleteTasks.map((t) => `[${t.severity}] ${t.description}`);
644
- return CONTINUE_PROMPT(
645
- modelCheck.modelResponse ?? "Model detected incomplete tasks",
646
- tasks.length > 0 ? tasks : ["Continue working"],
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(