@oh-my-pi/pi-coding-agent 17.2.5 → 17.2.6

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 (55) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/{CHANGELOG-q2w43aw3.md → CHANGELOG-gs76k6wc.md} +18 -0
  3. package/dist/cli.js +3378 -3333
  4. package/dist/types/cli/gc-cli.d.ts +1 -0
  5. package/dist/types/launch/client.d.ts +9 -1
  6. package/dist/types/launch/protocol.d.ts +17 -0
  7. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  8. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  9. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  10. package/dist/types/modes/interactive-mode.d.ts +4 -1
  11. package/dist/types/modes/types.d.ts +3 -1
  12. package/dist/types/security/contracts/schemas.d.ts +405 -403
  13. package/dist/types/session/agent-session-types.d.ts +5 -0
  14. package/dist/types/session/agent-session.d.ts +26 -2
  15. package/dist/types/session/launch-completion.d.ts +10 -0
  16. package/dist/types/session/session-entries.d.ts +15 -1
  17. package/dist/types/session/session-manager.d.ts +7 -0
  18. package/dist/types/session/yield-queue.d.ts +6 -1
  19. package/dist/types/tools/index.d.ts +9 -0
  20. package/package.json +12 -12
  21. package/src/cli/gc-cli.ts +641 -21
  22. package/src/config/model-discovery.ts +14 -14
  23. package/src/config/model-registry.ts +10 -8
  24. package/src/config/settings.ts +1 -1
  25. package/src/export/html/index.ts +10 -3
  26. package/src/export/share.ts +4 -0
  27. package/src/launch/broker.ts +222 -6
  28. package/src/launch/client.ts +161 -9
  29. package/src/launch/protocol.ts +52 -0
  30. package/src/main.ts +13 -6
  31. package/src/mcp/config-writer.ts +1 -1
  32. package/src/modes/components/btw-panel.ts +41 -4
  33. package/src/modes/controllers/btw-controller.ts +55 -7
  34. package/src/modes/controllers/command-controller.ts +31 -0
  35. package/src/modes/controllers/input-controller.ts +24 -7
  36. package/src/modes/interactive-mode.ts +16 -2
  37. package/src/modes/types.ts +8 -1
  38. package/src/prompts/session/launch-completion.md +1 -0
  39. package/src/sdk.ts +15 -0
  40. package/src/security/contracts/schemas.ts +205 -183
  41. package/src/security/contracts/validation.ts +5 -1
  42. package/src/security/store.ts +2 -2
  43. package/src/session/agent-session-types.ts +6 -0
  44. package/src/session/agent-session.ts +191 -14
  45. package/src/session/launch-completion.ts +37 -0
  46. package/src/session/session-context.ts +26 -0
  47. package/src/session/session-entries.ts +17 -1
  48. package/src/session/session-manager.ts +21 -0
  49. package/src/session/yield-queue.ts +121 -16
  50. package/src/slash-commands/builtin-registry.ts +10 -0
  51. package/src/tools/browser/cmux/cmux-tab.ts +92 -4
  52. package/src/tools/hub/launch.ts +122 -6
  53. package/src/tools/index.ts +9 -0
  54. package/dist/types/config/file-lock.d.ts +0 -29
  55. package/src/config/file-lock.ts +0 -164
@@ -250,7 +250,7 @@ export class InputController {
250
250
  this.#btwBranchListenerInstalled = true;
251
251
  this.ctx.ui.addInputListener(data => {
252
252
  if (!matchesKey(data, "b")) return undefined;
253
- if (!this.ctx.canBranchBtw()) return undefined;
253
+ if (!this.ctx.handlesBtwBranchKey()) return undefined;
254
254
  if (this.ctx.ui.getFocused() !== this.ctx.editor) return undefined;
255
255
  if (this.ctx.editor.getText().trim()) return undefined;
256
256
  void this.ctx.handleBtwBranchKey();
@@ -801,6 +801,19 @@ export class InputController {
801
801
  this.ctx.queueCompactionMessage(text, "steer", images);
802
802
  return;
803
803
  }
804
+ // Extension commands are local actions. Execute them before the normal
805
+ // submission path creates an optimistic user message; otherwise a
806
+ // consumed command remains rendered like a prompt sent to the model.
807
+ if (this.#isLocalExtensionCommand(text)) {
808
+ this.ctx.editor.clearDraft(text);
809
+ try {
810
+ await this.ctx.session.prompt(text, { images: inputImages });
811
+ } catch (error) {
812
+ this.ctx.editor.setText(text);
813
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
814
+ }
815
+ return;
816
+ }
804
817
 
805
818
  // If streaming, use prompt() with steer behavior
806
819
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
@@ -914,14 +927,18 @@ export class InputController {
914
927
  * Local extension commands are consumed before reaching the shared session
915
928
  * title gate and must not name the conversation.
916
929
  */
917
- #maybeStartTitleGeneration(text: string): void {
918
- const runner = this.ctx.session.extensionRunner;
930
+ #isLocalExtensionCommand(text: string): boolean {
919
931
  const extensionCommandSpace = text.indexOf(" ");
920
- const isLocalExtensionCommand =
932
+ return (
921
933
  text.startsWith("/") &&
922
- runner?.getCommand(extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace)) !==
923
- undefined;
924
- if (isLocalExtensionCommand) {
934
+ this.ctx.session.extensionRunner?.getCommand(
935
+ extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace),
936
+ ) !== undefined
937
+ );
938
+ }
939
+
940
+ #maybeStartTitleGeneration(text: string): void {
941
+ if (this.#isLocalExtensionCommand(text)) {
925
942
  return;
926
943
  }
927
944
  this.ctx.session.maybeStartTitleGeneration(text, () => {
@@ -4513,6 +4513,10 @@ export class InteractiveMode implements InteractiveModeContext {
4513
4513
  return this.#commandController.handleFreshCommand();
4514
4514
  }
4515
4515
 
4516
+ handleResetContextCommand(): Promise<void> {
4517
+ return this.#commandController.handleResetContextCommand();
4518
+ }
4519
+
4516
4520
  async handleDropCommand(): Promise<void> {
4517
4521
  if (this.#vibeSessionTransitionBlocked()) return;
4518
4522
  this.#prepareSessionSwitch();
@@ -4844,6 +4848,11 @@ export class InteractiveMode implements InteractiveModeContext {
4844
4848
  return this.#btwController.canBranch();
4845
4849
  }
4846
4850
 
4851
+ /** Reserves plain `b` only after /btw has a completed branch action to handle. */
4852
+ handlesBtwBranchKey(): boolean {
4853
+ return this.#btwController.handlesBranchKey();
4854
+ }
4855
+
4847
4856
  handleBtwBranchKey(): Promise<boolean> {
4848
4857
  return this.#btwController.handleBranch();
4849
4858
  }
@@ -4856,9 +4865,14 @@ export class InteractiveMode implements InteractiveModeContext {
4856
4865
  return this.#btwController.handleCopy();
4857
4866
  }
4858
4867
 
4859
- async handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void> {
4868
+ async handleBtwBranch(
4869
+ question: string,
4870
+ assistantMessage: AssistantMessage,
4871
+ leafId: string,
4872
+ sessionId: string,
4873
+ ): Promise<void> {
4860
4874
  try {
4861
- const result = await this.session.branchFromBtw(question, assistantMessage);
4875
+ const result = await this.session.branchFromBtw(question, assistantMessage, leafId, sessionId);
4862
4876
  if (result.cancelled) {
4863
4877
  this.showStatus("/btw branch cancelled", { dim: true });
4864
4878
  return;
@@ -350,6 +350,7 @@ export interface InteractiveModeContext {
350
350
  handleDebugTranscriptCommand(): Promise<void>;
351
351
  handleClearCommand(): Promise<void>;
352
352
  handleFreshCommand(): Promise<void>;
353
+ handleResetContextCommand(): Promise<void>;
353
354
  handleDropCommand(): Promise<void>;
354
355
  handleForkCommand(): Promise<void>;
355
356
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
@@ -418,9 +419,15 @@ export interface InteractiveModeContext {
418
419
  handleBtwEscape(): boolean;
419
420
  handleBtwBranchKey(): Promise<boolean>;
420
421
  canBranchBtw(): boolean;
422
+ handlesBtwBranchKey(): boolean;
421
423
  canCopyBtw(): boolean;
422
424
  handleBtwCopyKey(): Promise<boolean>;
423
- handleBtwBranch(question: string, assistantMessage: AssistantMessage): Promise<void>;
425
+ handleBtwBranch(
426
+ question: string,
427
+ assistantMessage: AssistantMessage,
428
+ leafId: string,
429
+ sessionId: string,
430
+ ): Promise<void>;
424
431
  handleOmfgCommand(complaint: string): Promise<void>;
425
432
  hasActiveOmfg(): boolean;
426
433
  handleOmfgEscape(): boolean;
@@ -0,0 +1 @@
1
+ Supervised process {{name}} {{state}} {{#if hasExitCode}}with exit code {{exitCode}}{{else}}without an exit code{{/if}}.
package/src/sdk.ts CHANGED
@@ -1686,6 +1686,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
1686
1686
  // entries capture it at fetch time and are dropped at injection if a newer
1687
1687
  // mutation (any tool) bumped it in the meantime.
1688
1688
  const fileMutationVersions = new Map<string, number>();
1689
+ const disposeCallbacks = new Set<() => void>();
1689
1690
  const activeToolNames = new Set<string>();
1690
1691
  const toolRegistry = new Map<string, Tool>();
1691
1692
  const setActiveToolNames = (names: Iterable<string>): void => {
@@ -1739,6 +1740,7 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
1739
1740
  trackEvalExecution: (execution, abortController) =>
1740
1741
  session ? session.trackEvalExecution(execution, abortController) : execution,
1741
1742
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
1743
+ isDisposed: () => session?.isDisposed ?? false,
1742
1744
  getHindsightSessionState: () => session?.getHindsightSessionState(),
1743
1745
  getMnemopiSessionState: () => session?.getMnemopiSessionState(),
1744
1746
  getAgentId: () => resolvedAgentId,
@@ -1765,6 +1767,14 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
1765
1767
  recordEvalSubagentUsage: output => sessionManager.recordEvalSubagentOutput(output),
1766
1768
  getClientBridge: () => session?.clientBridge,
1767
1769
  queueDeferredDiagnostics: entry => session?.yieldQueue.enqueue(LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE, entry),
1770
+ queueLaunchCompletion: notification =>
1771
+ session?.queueLaunchCompletion(notification) ??
1772
+ Promise.reject(new Error("Session unavailable for launch completion delivery")),
1773
+ registerDisposeCallback: callback => {
1774
+ disposeCallbacks.add(callback);
1775
+ return () => disposeCallbacks.delete(callback);
1776
+ },
1777
+ registerSessionChangeCallback: callback => session?.registerSessionChangeCallback(callback),
1768
1778
  bumpFileMutationVersion: path => {
1769
1779
  const next = (fileMutationVersions.get(path) ?? 0) + 1;
1770
1780
  fileMutationVersions.set(path, next);
@@ -3307,6 +3317,9 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
3307
3317
  const id = sessionManager.getSessionId?.();
3308
3318
  return id ? `${id}-advisor` : null;
3309
3319
  },
3320
+ queueLaunchCompletion: notification =>
3321
+ session?.queueLaunchCompletion(notification) ??
3322
+ Promise.reject(new Error("Session unavailable for launch completion delivery")),
3310
3323
  getAgentId: () => "advisor",
3311
3324
  // The primary's availability signals are wrong for advisors: their tool
3312
3325
  // slate is filtered separately at runtime (default read/grep/glob, no
@@ -3522,6 +3535,8 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
3522
3535
  unsubscribeCredentialDisabled?.();
3523
3536
  unsubscribeMcpNotifications?.();
3524
3537
  unregisterMcpPostmortem?.();
3538
+ for (const callback of disposeCallbacks) callback();
3539
+ disposeCallbacks.clear();
3525
3540
  // Drop refs so the process-global postmortem list doesn't retain
3526
3541
  // the bridge closure past explicit dispose.
3527
3542
  unsubscribeMcpNotifications = undefined;
@@ -1,201 +1,223 @@
1
- import { type } from "arktype";
1
+ import { once } from "@oh-my-pi/pi-utils";
2
+ import { scope } from "arktype";
2
3
 
3
- const stringRecordSchema = type({ "[string]": "string" });
4
- const unknownRecordSchema = type({ "[string]": "unknown" });
4
+ export const getSecurityContractSchemas = once(() => {
5
+ // Security schemas validate only during security scans, so lazy construction
6
+ // with interpreted traversal avoids eager JIT startup tax without changing correctness.
7
+ const { type } = scope({}, { jitless: true });
5
8
 
6
- export const securityProducerSchema = type({
7
- kind: "'omp-native' | 'codex-security-bundle' | 'codex-security-cloud' | 'sarif-import'",
8
- name: "string > 0",
9
- "version?": "string",
10
- "vendor?": "string",
11
- "revision?": "string",
12
- "pluginVersion?": "string",
13
- });
9
+ const stringRecordSchema = type({ "[string]": "string" });
10
+ const unknownRecordSchema = type({ "[string]": "unknown" });
14
11
 
15
- export const securityProvenanceSchema = type({
16
- producer: securityProducerSchema,
17
- createdAt: "string > 0",
18
- "importedAt?": "string",
19
- "sourceIds?": stringRecordSchema,
20
- "vendorFingerprints?": stringRecordSchema,
21
- "upstream?": {
22
- "repository?": "string",
12
+ const securityProducerSchema = type({
13
+ kind: "'omp-native' | 'codex-security-bundle' | 'codex-security-cloud' | 'sarif-import'",
14
+ name: "string > 0",
15
+ "version?": "string",
16
+ "vendor?": "string",
23
17
  "revision?": "string",
24
- "packageVersion?": "string",
25
18
  "pluginVersion?": "string",
26
- "archiveSha256?": "string",
27
- },
28
- "metadata?": unknownRecordSchema,
29
- });
19
+ });
30
20
 
31
- export const securityLocationSchema = type({
32
- path: "string > 0",
33
- startLine: "number.integer >= 1",
34
- "endLine?": "number.integer >= 1",
35
- "startColumn?": "number.integer >= 1",
36
- "endColumn?": "number.integer >= 1",
37
- "role?": "string",
38
- });
21
+ const securityProvenanceSchema = type({
22
+ producer: securityProducerSchema,
23
+ createdAt: "string > 0",
24
+ "importedAt?": "string",
25
+ "sourceIds?": stringRecordSchema,
26
+ "vendorFingerprints?": stringRecordSchema,
27
+ "upstream?": {
28
+ "repository?": "string",
29
+ "revision?": "string",
30
+ "packageVersion?": "string",
31
+ "pluginVersion?": "string",
32
+ "archiveSha256?": "string",
33
+ },
34
+ "metadata?": unknownRecordSchema,
35
+ });
39
36
 
40
- export const securityEvidenceSchema = type({
41
- id: "string > 0",
42
- kind: "'code' | 'trace' | 'validation' | 'note'",
43
- label: "string > 0",
44
- explanation: "string",
45
- "location?": securityLocationSchema,
46
- "excerpt?": "string",
47
- });
37
+ const securityLocationSchema = type({
38
+ path: "string > 0",
39
+ startLine: "number.integer >= 1",
40
+ "endLine?": "number.integer >= 1",
41
+ "startColumn?": "number.integer >= 1",
42
+ "endColumn?": "number.integer >= 1",
43
+ "role?": "string",
44
+ });
48
45
 
49
- export const securityOccurrenceSchema = type({
50
- id: "string > 0",
51
- locations: securityLocationSchema.array().atLeastLength(1),
52
- evidenceIds: "string[]",
53
- });
46
+ const securityEvidenceSchema = type({
47
+ id: "string > 0",
48
+ kind: "'code' | 'trace' | 'validation' | 'note'",
49
+ label: "string > 0",
50
+ explanation: "string",
51
+ "location?": securityLocationSchema,
52
+ "excerpt?": "string",
53
+ });
54
54
 
55
- export const securityFindingSchema = type({
56
- id: "string > 0",
57
- scanId: "string > 0",
58
- fingerprint: "string > 0",
59
- ruleId: "string > 0",
60
- "anchor?": "string",
61
- title: "string > 0",
62
- summary: "string",
63
- severity: {
64
- level: "'critical' | 'high' | 'medium' | 'low' | 'informational'",
65
- "score?": "number",
66
- "scoringSystem?": "string",
67
- "vector?": "string",
68
- "rationale?": "string",
69
- },
70
- confidence: {
71
- level: "'high' | 'medium' | 'low'",
72
- "rationale?": "string",
73
- },
74
- taxonomy: {
75
- category: "string > 0",
76
- cwe: "string[]",
77
- "tags?": "string[]",
78
- },
79
- occurrences: securityOccurrenceSchema.array().atLeastLength(1),
80
- evidence: securityEvidenceSchema.array(),
81
- "remediation?": "string",
82
- validation: {
83
- status: "'unvalidated' | 'validated' | 'rejected' | 'partial' | 'error'",
84
- "summary?": "string",
55
+ const securityOccurrenceSchema = type({
56
+ id: "string > 0",
57
+ locations: securityLocationSchema.array().atLeastLength(1),
85
58
  evidenceIds: "string[]",
86
- "validatedAt?": "string",
87
- },
88
- disposition: {
89
- status: "'open' | 'false_positive' | 'accepted_risk' | 'fixed' | 'wont_fix'",
90
- "rationale?": "string",
91
- "updatedAt?": "string",
92
- "actor?": "string",
93
- },
94
- provenance: securityProvenanceSchema,
95
- "extensions?": unknownRecordSchema,
96
- });
59
+ });
97
60
 
98
- export const securityCoverageSchema = type({
99
- mode: "'repository' | 'scoped_path' | 'diff' | 'working_tree' | 'deep_repository' | 'imported'",
100
- completeness: "'complete' | 'partial' | 'unknown'",
101
- inventoryStrategy: "'repository' | 'scoped_path' | 'diff' | 'directory' | 'custom' | 'imported'",
102
- includePaths: "string[]",
103
- excludePaths: "string[]",
104
- surfaces: type({
105
- id: "string > 0",
106
- label: "string > 0",
107
- disposition: "'reported' | 'no_issue_found' | 'rejected' | 'not_applicable' | 'needs_follow_up'",
108
- receiptRefs: "string[]",
109
- "riskArea?": "string",
110
- "notes?": "string",
111
- }).array(),
112
- explicitExclusions: type({ pattern: "string", reason: "string" }).array(),
113
- deferred: type({
61
+ const securityFindingSchema = type({
114
62
  id: "string > 0",
115
- reason: "string > 0",
116
- "paths?": "string[]",
117
- "surfaceIds?": "string[]",
118
- }).array(),
119
- "openQuestions?": type({ question: "string > 0", "followUpPrompt?": "string" }).array(),
120
- });
63
+ scanId: "string > 0",
64
+ fingerprint: "string > 0",
65
+ ruleId: "string > 0",
66
+ "anchor?": "string",
67
+ title: "string > 0",
68
+ summary: "string",
69
+ severity: {
70
+ level: "'critical' | 'high' | 'medium' | 'low' | 'informational'",
71
+ "score?": "number",
72
+ "scoringSystem?": "string",
73
+ "vector?": "string",
74
+ "rationale?": "string",
75
+ },
76
+ confidence: {
77
+ level: "'high' | 'medium' | 'low'",
78
+ "rationale?": "string",
79
+ },
80
+ taxonomy: {
81
+ category: "string > 0",
82
+ cwe: "string[]",
83
+ "tags?": "string[]",
84
+ },
85
+ occurrences: securityOccurrenceSchema.array().atLeastLength(1),
86
+ evidence: securityEvidenceSchema.array(),
87
+ "remediation?": "string",
88
+ validation: {
89
+ status: "'unvalidated' | 'validated' | 'rejected' | 'partial' | 'error'",
90
+ "summary?": "string",
91
+ evidenceIds: "string[]",
92
+ "validatedAt?": "string",
93
+ },
94
+ disposition: {
95
+ status: "'open' | 'false_positive' | 'accepted_risk' | 'fixed' | 'wont_fix'",
96
+ "rationale?": "string",
97
+ "updatedAt?": "string",
98
+ "actor?": "string",
99
+ },
100
+ provenance: securityProvenanceSchema,
101
+ "extensions?": unknownRecordSchema,
102
+ });
121
103
 
122
- export const securityTargetSchema = type({
123
- kind: "'repository' | 'scoped_path' | 'ref_diff' | 'working_tree' | 'imported'",
124
- repositoryRoot: "string > 0",
125
- displayName: "string > 0",
126
- "revision?": "string",
127
- "baseRevision?": "string",
128
- "headRevision?": "string",
129
- includePaths: "string[]",
130
- excludePaths: "string[]",
131
- treeDigest: "string > 0",
132
- });
104
+ const securityCoverageSchema = type({
105
+ mode: "'repository' | 'scoped_path' | 'diff' | 'working_tree' | 'deep_repository' | 'imported'",
106
+ completeness: "'complete' | 'partial' | 'unknown'",
107
+ inventoryStrategy: "'repository' | 'scoped_path' | 'diff' | 'directory' | 'custom' | 'imported'",
108
+ includePaths: "string[]",
109
+ excludePaths: "string[]",
110
+ surfaces: type({
111
+ id: "string > 0",
112
+ label: "string > 0",
113
+ disposition: "'reported' | 'no_issue_found' | 'rejected' | 'not_applicable' | 'needs_follow_up'",
114
+ receiptRefs: "string[]",
115
+ "riskArea?": "string",
116
+ "notes?": "string",
117
+ }).array(),
118
+ explicitExclusions: type({ pattern: "string", reason: "string" }).array(),
119
+ deferred: type({
120
+ id: "string > 0",
121
+ reason: "string > 0",
122
+ "paths?": "string[]",
123
+ "surfaceIds?": "string[]",
124
+ }).array(),
125
+ "openQuestions?": type({ question: "string > 0", "followUpPrompt?": "string" }).array(),
126
+ });
133
127
 
134
- export const securityScanPlanSchema = type({
135
- documentType: "'omp-security.scan-plan'",
136
- schemaVersion: "'1.0'",
137
- id: "string > 0",
138
- createdAt: "string > 0",
139
- repositoryRoot: "string > 0",
140
- target: securityTargetSchema,
141
- knowledgeBases: type({ path: "string > 0", sha256: "string > 0", size: "number.integer >= 0" }).array(),
142
- output: {
143
- root: "string > 0",
144
- archiveExisting: "boolean",
145
- existingState: "'absent' | 'empty' | 'archivable'",
146
- },
147
- model: { provider: "string > 0", modelId: "string > 0", "thinkingLevel?": "string" },
148
- account: {
149
- provider: "string > 0",
150
- credentialId: "number.integer >= 1",
151
- "accountId?": "string",
152
- "email?": "string",
153
- "organizationId?": "string",
154
- "organizationName?": "string",
155
- },
156
- configFingerprint: "string > 0",
157
- workflowFingerprint: "string > 0",
158
- fingerprint: "string > 0",
159
- });
128
+ const securityTargetSchema = type({
129
+ kind: "'repository' | 'scoped_path' | 'ref_diff' | 'working_tree' | 'imported'",
130
+ repositoryRoot: "string > 0",
131
+ displayName: "string > 0",
132
+ "revision?": "string",
133
+ "baseRevision?": "string",
134
+ "headRevision?": "string",
135
+ includePaths: "string[]",
136
+ excludePaths: "string[]",
137
+ treeDigest: "string > 0",
138
+ });
160
139
 
161
- export const securityScanMetricsSchema = type({
162
- "runtimeMs?": "number >= 0",
163
- "tokenUsage?": {
164
- input: "number >= 0",
165
- output: "number >= 0",
166
- reasoning: "number >= 0",
167
- cacheRead: "number >= 0",
168
- cacheWrite: "number >= 0",
169
- total: "number >= 0",
170
- },
171
- "cost?": "number >= 0",
172
- "premiumRequests?": "number >= 0",
173
- });
140
+ const securityScanPlanSchema = type({
141
+ documentType: "'omp-security.scan-plan'",
142
+ schemaVersion: "'1.0'",
143
+ id: "string > 0",
144
+ createdAt: "string > 0",
145
+ repositoryRoot: "string > 0",
146
+ target: securityTargetSchema,
147
+ knowledgeBases: type({ path: "string > 0", sha256: "string > 0", size: "number.integer >= 0" }).array(),
148
+ output: {
149
+ root: "string > 0",
150
+ archiveExisting: "boolean",
151
+ existingState: "'absent' | 'empty' | 'archivable'",
152
+ },
153
+ model: { provider: "string > 0", modelId: "string > 0", "thinkingLevel?": "string" },
154
+ account: {
155
+ provider: "string > 0",
156
+ credentialId: "number.integer >= 1",
157
+ "accountId?": "string",
158
+ "email?": "string",
159
+ "organizationId?": "string",
160
+ "organizationName?": "string",
161
+ },
162
+ configFingerprint: "string > 0",
163
+ workflowFingerprint: "string > 0",
164
+ fingerprint: "string > 0",
165
+ });
174
166
 
175
- export const securityScanSchema = type({
176
- documentType: "'omp-security.scan'",
177
- schemaVersion: "'1.0'",
178
- id: "string > 0",
179
- projectKey: "string > 0",
180
- status: "'planned' | 'running' | 'completed' | 'partial' | 'cancelled' | 'failed'",
181
- createdAt: "string > 0",
182
- "startedAt?": "string",
183
- "completedAt?": "string",
184
- "plan?": securityScanPlanSchema,
185
- target: securityTargetSchema,
186
- producer: securityProducerSchema,
187
- provenance: securityProvenanceSchema,
188
- findingIds: "string[]",
189
- coverage: securityCoverageSchema,
190
- "reportRef?": "string",
191
- "sarifRef?": "string",
192
- "error?": "string",
193
- "metrics?": securityScanMetricsSchema,
194
- });
167
+ const securityScanMetricsSchema = type({
168
+ "runtimeMs?": "number >= 0",
169
+ "tokenUsage?": {
170
+ input: "number >= 0",
171
+ output: "number >= 0",
172
+ reasoning: "number >= 0",
173
+ cacheRead: "number >= 0",
174
+ cacheWrite: "number >= 0",
175
+ total: "number >= 0",
176
+ },
177
+ "cost?": "number >= 0",
178
+ "premiumRequests?": "number >= 0",
179
+ });
180
+
181
+ const securityScanSchema = type({
182
+ documentType: "'omp-security.scan'",
183
+ schemaVersion: "'1.0'",
184
+ id: "string > 0",
185
+ projectKey: "string > 0",
186
+ status: "'planned' | 'running' | 'completed' | 'partial' | 'cancelled' | 'failed'",
187
+ createdAt: "string > 0",
188
+ "startedAt?": "string",
189
+ "completedAt?": "string",
190
+ "plan?": securityScanPlanSchema,
191
+ target: securityTargetSchema,
192
+ producer: securityProducerSchema,
193
+ provenance: securityProvenanceSchema,
194
+ findingIds: "string[]",
195
+ coverage: securityCoverageSchema,
196
+ "reportRef?": "string",
197
+ "sarifRef?": "string",
198
+ "error?": "string",
199
+ "metrics?": securityScanMetricsSchema,
200
+ });
201
+
202
+ const securityScanBundleSchema = type({
203
+ scan: securityScanSchema,
204
+ findings: securityFindingSchema.array(),
205
+ "report?": "string",
206
+ "sarif?": unknownRecordSchema,
207
+ });
195
208
 
196
- export const securityScanBundleSchema = type({
197
- scan: securityScanSchema,
198
- findings: securityFindingSchema.array(),
199
- "report?": "string",
200
- "sarif?": unknownRecordSchema,
209
+ return {
210
+ securityProducerSchema,
211
+ securityProvenanceSchema,
212
+ securityLocationSchema,
213
+ securityEvidenceSchema,
214
+ securityOccurrenceSchema,
215
+ securityFindingSchema,
216
+ securityCoverageSchema,
217
+ securityTargetSchema,
218
+ securityScanPlanSchema,
219
+ securityScanMetricsSchema,
220
+ securityScanSchema,
221
+ securityScanBundleSchema,
222
+ };
201
223
  });
@@ -1,5 +1,5 @@
1
1
  import { type } from "arktype";
2
- import { securityFindingSchema, securityScanBundleSchema, securityScanPlanSchema, securityScanSchema } from "./schemas";
2
+ import { getSecurityContractSchemas } from "./schemas";
3
3
  import type { SecurityFinding, SecurityScan, SecurityScanBundle, SecurityScanPlan } from "./types";
4
4
 
5
5
  function schemaError(label: string, errors: type.errors): Error {
@@ -7,24 +7,28 @@ function schemaError(label: string, errors: type.errors): Error {
7
7
  }
8
8
 
9
9
  export function parseSecurityFinding(value: unknown): SecurityFinding {
10
+ const { securityFindingSchema } = getSecurityContractSchemas();
10
11
  const result = securityFindingSchema(value);
11
12
  if (result instanceof type.errors) throw schemaError("Security finding", result);
12
13
  return result as SecurityFinding;
13
14
  }
14
15
 
15
16
  export function parseSecurityScan(value: unknown): SecurityScan {
17
+ const { securityScanSchema } = getSecurityContractSchemas();
16
18
  const result = securityScanSchema(value);
17
19
  if (result instanceof type.errors) throw schemaError("Security scan", result);
18
20
  return result as SecurityScan;
19
21
  }
20
22
 
21
23
  export function parseSecurityScanPlan(value: unknown): SecurityScanPlan {
24
+ const { securityScanPlanSchema } = getSecurityContractSchemas();
22
25
  const result = securityScanPlanSchema(value);
23
26
  if (result instanceof type.errors) throw schemaError("Security scan plan", result);
24
27
  return result as SecurityScanPlan;
25
28
  }
26
29
 
27
30
  export function parseSecurityScanBundle(value: unknown): SecurityScanBundle {
31
+ const { securityScanBundleSchema } = getSecurityContractSchemas();
28
32
  const result = securityScanBundleSchema(value);
29
33
  if (result instanceof type.errors) throw schemaError("Security scan bundle", result);
30
34
  const bundle = result as SecurityScanBundle;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { getSecurityProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
4
- import { withFileLock } from "../config/file-lock";
4
+ import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
5
5
  import * as git from "../utils/git";
6
6
  import { compareSecurityLineage } from "./comparison";
7
7
  import type {
@@ -34,7 +34,7 @@ const SECURITY_STORE_WRITE_CHAINS = new Map<string, Promise<unknown>>();
34
34
  async function withSecurityStoreWrite<T>(key: string, operation: () => Promise<T>): Promise<T> {
35
35
  const lockTarget = path.join(key, "index.json");
36
36
  const run = (SECURITY_STORE_WRITE_CHAINS.get(key) ?? Promise.resolve()).then(() =>
37
- withFileLock(lockTarget, operation, { staleMs: 60_000, retries: 200, retryDelayMs: 50 }),
37
+ withFileLock(lockTarget, operation, { retries: 200, retryDelayMs: 50 }),
38
38
  );
39
39
  const guarded = run.catch(() => undefined);
40
40
  SECURITY_STORE_WRITE_CHAINS.set(key, guarded);
@@ -389,5 +389,11 @@ export interface FreshSessionResult {
389
389
  closedProviderSessions: number;
390
390
  }
391
391
 
392
+ /** Outcome of an in-place `/reset` conversation-context reset. */
393
+ export interface ResetSessionContextResult {
394
+ /** Number of live messages dropped from the model's context. */
395
+ droppedCount: number;
396
+ }
397
+
392
398
  /** Queued user content restored to the editor. */
393
399
  export type RestoredQueuedMessage = { text: string; images?: ImageContent[] };