@oss-autopilot/core 3.2.0 → 3.4.0

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 (68) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.js +39 -3
  3. package/dist/cli.bundle.cjs +103 -75
  4. package/dist/cli.js +17 -3
  5. package/dist/commands/check-integration.js +8 -8
  6. package/dist/commands/comments.js +3 -0
  7. package/dist/commands/config.js +14 -7
  8. package/dist/commands/daily-render.js +10 -5
  9. package/dist/commands/daily.d.ts +3 -9
  10. package/dist/commands/daily.js +12 -21
  11. package/dist/commands/dashboard-data.js +1 -1
  12. package/dist/commands/dashboard-lifecycle.js +1 -1
  13. package/dist/commands/dashboard-process.js +4 -4
  14. package/dist/commands/dashboard-server.js +26 -7
  15. package/dist/commands/dashboard.js +2 -2
  16. package/dist/commands/detect-formatters.js +3 -3
  17. package/dist/commands/doctor.js +5 -5
  18. package/dist/commands/guidelines.d.ts +10 -0
  19. package/dist/commands/guidelines.js +25 -6
  20. package/dist/commands/list-move-tier.js +5 -5
  21. package/dist/commands/local-repos.js +9 -9
  22. package/dist/commands/parse-list.js +10 -10
  23. package/dist/commands/scout-bridge.js +2 -2
  24. package/dist/commands/setup.js +24 -13
  25. package/dist/commands/skip-add.js +6 -3
  26. package/dist/commands/skip-file-parser.js +3 -3
  27. package/dist/commands/startup.js +11 -8
  28. package/dist/commands/state-cmd.js +1 -1
  29. package/dist/commands/status.js +7 -0
  30. package/dist/commands/validation.js +12 -3
  31. package/dist/commands/vet-list.js +12 -8
  32. package/dist/commands/vet.js +1 -2
  33. package/dist/core/__fixtures__/prompt-injection-payloads.d.ts +22 -0
  34. package/dist/core/__fixtures__/prompt-injection-payloads.js +109 -0
  35. package/dist/core/anti-llm-policy.js +5 -5
  36. package/dist/core/auth.js +12 -8
  37. package/dist/core/daily-logic.d.ts +13 -1
  38. package/dist/core/daily-logic.js +31 -4
  39. package/dist/core/dates.js +3 -3
  40. package/dist/core/errors.d.ts +29 -0
  41. package/dist/core/errors.js +63 -0
  42. package/dist/core/formatter-detection.js +9 -9
  43. package/dist/core/gist-state-store.d.ts +42 -3
  44. package/dist/core/gist-state-store.js +89 -19
  45. package/dist/core/guidelines-store.js +2 -2
  46. package/dist/core/http-cache.js +16 -7
  47. package/dist/core/index.d.ts +3 -1
  48. package/dist/core/index.js +6 -1
  49. package/dist/core/issue-conversation.js +3 -1
  50. package/dist/core/paths.js +4 -4
  51. package/dist/core/placeholder-usernames.d.ts +1 -0
  52. package/dist/core/placeholder-usernames.js +27 -0
  53. package/dist/core/pr-comments-fetcher.d.ts +14 -6
  54. package/dist/core/pr-comments-fetcher.js +8 -14
  55. package/dist/core/pr-monitor.d.ts +0 -2
  56. package/dist/core/pr-monitor.js +2 -25
  57. package/dist/core/pr-template.js +1 -1
  58. package/dist/core/state-persistence.d.ts +2 -2
  59. package/dist/core/state-persistence.js +15 -12
  60. package/dist/core/state-schema.js +8 -4
  61. package/dist/core/state.d.ts +27 -0
  62. package/dist/core/state.js +71 -14
  63. package/dist/core/untrusted-content.d.ts +48 -0
  64. package/dist/core/untrusted-content.js +106 -0
  65. package/dist/core/urls.js +2 -2
  66. package/dist/formatters/json.d.ts +53 -3
  67. package/dist/formatters/json.js +49 -14
  68. package/package.json +3 -3
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Wrap GitHub-sourced text (PR titles, PR bodies, issue bodies, review
3
+ * comments) in a fenced delimiter so the LLM treats it as data, not
4
+ * instructions (#1192).
5
+ *
6
+ * The contract this module pins, exercised by `untrusted-content.test.ts`
7
+ * and the prompt-injection corpus:
8
+ *
9
+ * 1. Output starts with the open tag and ends with the close tag.
10
+ * 2. The close-tag literal NEVER appears inside the wrapped body — any
11
+ * occurrence in the input is escaped to a sentinel so an attacker
12
+ * cannot close the fence early and inject instructions after it.
13
+ * 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
14
+ * unchanged for any input — the wrapping is lossless.
15
+ *
16
+ * Consumers should pair this with the agent-side guidance in
17
+ * `workflows/reference.md` ("Prompt Injection Awareness"), which tells
18
+ * the LLM to ignore instructions inside `<github-content>` blocks.
19
+ *
20
+ * Non-goals:
21
+ * - This is NOT a content filter. We do not detect or strip prompt-
22
+ * injection payloads; the human-in-the-loop gate on `post`/`claim`
23
+ * remains the primary control.
24
+ */
25
+ export declare const UNTRUSTED_OPEN_TAG_NAME = "github-content";
26
+ export declare const UNTRUSTED_CLOSE_TAG = "</github-content>";
27
+ export interface UntrustedContentMeta {
28
+ /** GitHub login of the content's author (e.g. PR comment author). */
29
+ author?: string;
30
+ /** GitHub author_association (OWNER, MEMBER, CONTRIBUTOR, NONE, ...). */
31
+ association?: string;
32
+ /** Free-form provenance label (e.g. "pr-body", "review-comment"). */
33
+ source?: string;
34
+ }
35
+ /**
36
+ * Wrap `text` in a `<github-content>` fence labeled with `label` and the
37
+ * optional metadata. Any occurrence of the close-tag literal inside `text`
38
+ * is replaced with a zero-width-joined sentinel that round-trips losslessly
39
+ * via {@link extractFromFence}.
40
+ */
41
+ export declare function wrapUntrustedContent(text: string, label: string, meta?: UntrustedContentMeta): string;
42
+ /**
43
+ * Reverse of {@link wrapUntrustedContent}. Extracts the original body text
44
+ * (un-escaping any sentinel-encoded close tags). Throws if the input is not
45
+ * a single well-formed fence — callers shouldn't be parsing arbitrary
46
+ * markdown with this; it's for tests + symmetric reasoning only.
47
+ */
48
+ export declare function extractFromFence(fenced: string): string;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Wrap GitHub-sourced text (PR titles, PR bodies, issue bodies, review
3
+ * comments) in a fenced delimiter so the LLM treats it as data, not
4
+ * instructions (#1192).
5
+ *
6
+ * The contract this module pins, exercised by `untrusted-content.test.ts`
7
+ * and the prompt-injection corpus:
8
+ *
9
+ * 1. Output starts with the open tag and ends with the close tag.
10
+ * 2. The close-tag literal NEVER appears inside the wrapped body — any
11
+ * occurrence in the input is escaped to a sentinel so an attacker
12
+ * cannot close the fence early and inject instructions after it.
13
+ * 3. `extractFromFence(wrapUntrustedContent(x, label))` returns `x`
14
+ * unchanged for any input — the wrapping is lossless.
15
+ *
16
+ * Consumers should pair this with the agent-side guidance in
17
+ * `workflows/reference.md` ("Prompt Injection Awareness"), which tells
18
+ * the LLM to ignore instructions inside `<github-content>` blocks.
19
+ *
20
+ * Non-goals:
21
+ * - This is NOT a content filter. We do not detect or strip prompt-
22
+ * injection payloads; the human-in-the-loop gate on `post`/`claim`
23
+ * remains the primary control.
24
+ */
25
+ export const UNTRUSTED_OPEN_TAG_NAME = 'github-content';
26
+ export const UNTRUSTED_CLOSE_TAG = `</${UNTRUSTED_OPEN_TAG_NAME}>`;
27
+ /**
28
+ * Sentinels used to neutralize any literal open- or close-tag substring
29
+ * inside the wrapped body. We use HTML entity escapes (`&lt;` / `&gt;`) so
30
+ * the result is pure ASCII, lints cleanly, and round-trips losslessly. The
31
+ * `&` itself is escaped first so an input containing the literal text
32
+ * `&lt;/github-content&gt;` survives the wrap/unwrap round-trip without
33
+ * collision.
34
+ */
35
+ const AMP_ESCAPE = '&amp;';
36
+ const CLOSE_TAG_ESCAPE = `&lt;/${UNTRUSTED_OPEN_TAG_NAME}&gt;`;
37
+ const OPEN_TAG_PATTERN = new RegExp(`<${UNTRUSTED_OPEN_TAG_NAME}\\b`, 'g');
38
+ const OPEN_TAG_ESCAPE = `&lt;${UNTRUSTED_OPEN_TAG_NAME}`;
39
+ function escapeAttr(value) {
40
+ // Newlines/carriage returns can't break out of a quoted attribute (no `"`)
41
+ // but they visually fragment the open tag in the prompt text the LLM
42
+ // reads, so encode them as numeric entities for defense-in-depth.
43
+ return value
44
+ .replace(/&/g, '&amp;')
45
+ .replace(/"/g, '&quot;')
46
+ .replace(/</g, '&lt;')
47
+ .replace(/>/g, '&gt;')
48
+ .replace(/\n/g, '&#10;')
49
+ .replace(/\r/g, '&#13;');
50
+ }
51
+ /**
52
+ * Wrap `text` in a `<github-content>` fence labeled with `label` and the
53
+ * optional metadata. Any occurrence of the close-tag literal inside `text`
54
+ * is replaced with a zero-width-joined sentinel that round-trips losslessly
55
+ * via {@link extractFromFence}.
56
+ */
57
+ export function wrapUntrustedContent(text, label, meta = {}) {
58
+ // Escape order matters for round-trip correctness:
59
+ // 1. `&` first — so any pre-existing entity in the input gets a literal
60
+ // `&amp;` that survives the unwrap pass below.
61
+ // 2. Close-tag literals — replaced with the entity-escaped form.
62
+ // 3. Open-tag literals (matched only at boundaries via OPEN_TAG_PATTERN
63
+ // so we don't accidentally rewrite the close-tag's `</github-content`).
64
+ const escapedBody = text
65
+ .split('&')
66
+ .join(AMP_ESCAPE)
67
+ .split(UNTRUSTED_CLOSE_TAG)
68
+ .join(CLOSE_TAG_ESCAPE)
69
+ .replace(OPEN_TAG_PATTERN, OPEN_TAG_ESCAPE);
70
+ const attrs = [`label="${escapeAttr(label)}"`];
71
+ if (meta.author !== undefined)
72
+ attrs.push(`author="${escapeAttr(meta.author)}"`);
73
+ if (meta.association !== undefined)
74
+ attrs.push(`association="${escapeAttr(meta.association)}"`);
75
+ if (meta.source !== undefined)
76
+ attrs.push(`source="${escapeAttr(meta.source)}"`);
77
+ return `<${UNTRUSTED_OPEN_TAG_NAME} ${attrs.join(' ')}>${escapedBody}${UNTRUSTED_CLOSE_TAG}`;
78
+ }
79
+ /**
80
+ * Reverse of {@link wrapUntrustedContent}. Extracts the original body text
81
+ * (un-escaping any sentinel-encoded close tags). Throws if the input is not
82
+ * a single well-formed fence — callers shouldn't be parsing arbitrary
83
+ * markdown with this; it's for tests + symmetric reasoning only.
84
+ */
85
+ export function extractFromFence(fenced) {
86
+ const openMatch = fenced.match(new RegExp(`^<${UNTRUSTED_OPEN_TAG_NAME}\\b[^>]*>`));
87
+ if (!openMatch) {
88
+ throw new Error('extractFromFence: input does not start with a <github-content> open tag');
89
+ }
90
+ if (!fenced.endsWith(UNTRUSTED_CLOSE_TAG)) {
91
+ throw new Error('extractFromFence: input does not end with </github-content>');
92
+ }
93
+ const inner = fenced.slice(openMatch[0].length, fenced.length - UNTRUSTED_CLOSE_TAG.length);
94
+ if (inner.includes(UNTRUSTED_CLOSE_TAG)) {
95
+ throw new Error('extractFromFence: nested </github-content> found in body — fence escaping is broken');
96
+ }
97
+ // Reverse the escapes in opposite order: tag entities first (so an
98
+ // already-`&amp;`-prefixed entity survives), then unescape `&amp;`.
99
+ return inner
100
+ .split(CLOSE_TAG_ESCAPE)
101
+ .join(UNTRUSTED_CLOSE_TAG)
102
+ .split(OPEN_TAG_ESCAPE)
103
+ .join(`<${UNTRUSTED_OPEN_TAG_NAME}`)
104
+ .split(AMP_ESCAPE)
105
+ .join('&');
106
+ }
package/dist/core/urls.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * owner/repo characters. Extracted from utils.ts under #1116.
6
6
  */
7
7
  // Validation patterns for GitHub owner and repo names
8
- const OWNER_PATTERN = /^[a-zA-Z0-9_-]+$/;
9
- const REPO_PATTERN = /^[a-zA-Z0-9_.-]+$/;
8
+ const OWNER_PATTERN = /^[\w-]+$/;
9
+ const REPO_PATTERN = /^[\w.-]+$/;
10
10
  function isValidOwnerRepo(owner, repo) {
11
11
  return OWNER_PATTERN.test(owner) && REPO_PATTERN.test(repo);
12
12
  }
@@ -3,10 +3,9 @@
3
3
  * Provides structured output that can be consumed by scripts and plugins
4
4
  */
5
5
  import { z, type ZodType } from 'zod';
6
- import type { FetchedPR, DailyDigest, AgentState, RepoGroup, CommentedIssue, ShelvedPRRef } from '../core/types.js';
6
+ import type { FetchedPR, DailyDigest, AgentState, RepoGroup, CommentedIssue, ShelvedPRRef, SearchPriority, CapacityAssessment, ActionableIssue, ActionableIssueType, CompactActionableIssue, ActionMenuItem, ActionMenu } from '../core/types.js';
7
7
  import type { ContributionStats } from '../core/stats.js';
8
8
  import type { PRCheckFailure } from '../core/pr-monitor.js';
9
- import type { SearchPriority, CapacityAssessment, ActionableIssue, ActionableIssueType, CompactActionableIssue, ActionMenuItem, ActionMenu } from '../core/types.js';
10
9
  import type { CIFormatterDiagnosis, FormatterDetectionResult } from '../core/formatter-detection.js';
11
10
  export type { CapacityAssessment, ActionableIssue, ActionableIssueType, CompactActionableIssue, ActionMenuItem, ActionMenu, };
12
11
  export type ErrorCode = 'AUTH_REQUIRED' | 'RATE_LIMITED' | 'VALIDATION' | 'CONFIGURATION' | 'NETWORK' | 'NOT_FOUND' | 'STATE_CORRUPTED' | 'CONCURRENCY' | 'UNKNOWN';
@@ -51,18 +50,37 @@ export interface CompactRepoGroup {
51
50
  * See `DailyWarning` and issue #1042 for the rationale — keeping this a
52
51
  * fixed union so downstream consumers can switch on it without drift.
53
52
  */
54
- export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint';
53
+ export type DailyWarningPhase = 'fetch' | 'repo-scores' | 'analytics' | 'scout-sync' | 'partition' | 'dismiss-filter' | 'gist-checkpoint' | 'gist-staleness';
55
54
  /**
56
55
  * A single non-fatal failure surfaced from the `daily` pipeline. Unlike
57
56
  * `PRCheckFailure` (which is scoped to per-PR fetch errors), this covers
58
57
  * ancillary fetches that previously demoted to a log-only `warn()` — repo
59
58
  * metadata, monthly analytics, scout sync, Gist checkpoint, etc.
59
+ *
60
+ * `timestamp` and `details` are optional structured extensions added in
61
+ * #1193 so staleness warnings can carry `lastSuccessfulRefresh` /
62
+ * `detectedAt` without bespoke schemas. Existing producers don't need to
63
+ * supply them.
60
64
  */
61
65
  export interface DailyWarning {
62
66
  phase: DailyWarningPhase;
63
67
  operation: string;
64
68
  message: string;
69
+ timestamp?: string;
70
+ details?: Record<string, unknown>;
65
71
  }
72
+ /**
73
+ * Build a warning entry from a {@link StalenessInfo} marker (#1193).
74
+ * Co-located with `DailyWarning` so every command (`daily`, `status`,
75
+ * `comments`) builds the same shape from the same source.
76
+ */
77
+ export interface StalenessLike {
78
+ source: string;
79
+ reason: string;
80
+ lastSuccessfulRefresh: string | null;
81
+ detectedAt: string;
82
+ }
83
+ export declare function buildStalenessWarning(info: StalenessLike): DailyWarning;
66
84
  export interface DailyOutput {
67
85
  digest: DailyDigestCompact;
68
86
  capacity: CapacityAssessment;
@@ -139,6 +157,22 @@ export declare const StatusOutputSchema: z.ZodObject<{
139
157
  lastRunAt: z.ZodString;
140
158
  offline: z.ZodOptional<z.ZodBoolean>;
141
159
  lastUpdated: z.ZodOptional<z.ZodString>;
160
+ warnings: z.ZodOptional<z.ZodArray<z.ZodObject<{
161
+ phase: z.ZodEnum<{
162
+ fetch: "fetch";
163
+ "repo-scores": "repo-scores";
164
+ analytics: "analytics";
165
+ "scout-sync": "scout-sync";
166
+ partition: "partition";
167
+ "dismiss-filter": "dismiss-filter";
168
+ "gist-checkpoint": "gist-checkpoint";
169
+ "gist-staleness": "gist-staleness";
170
+ }>;
171
+ operation: z.ZodString;
172
+ message: z.ZodString;
173
+ timestamp: z.ZodOptional<z.ZodString>;
174
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
175
+ }, z.core.$strip>>>;
142
176
  }, z.core.$strip>;
143
177
  export type StatusOutput = z.infer<typeof StatusOutputSchema>;
144
178
  export declare const DailyOutputSchema: z.ZodObject<{
@@ -234,9 +268,12 @@ export declare const DailyOutputSchema: z.ZodObject<{
234
268
  partition: "partition";
235
269
  "dismiss-filter": "dismiss-filter";
236
270
  "gist-checkpoint": "gist-checkpoint";
271
+ "gist-staleness": "gist-staleness";
237
272
  }>;
238
273
  operation: z.ZodString;
239
274
  message: z.ZodString;
275
+ timestamp: z.ZodOptional<z.ZodString>;
276
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
240
277
  }, z.core.$strip>>;
241
278
  }, z.core.$strip>;
242
279
  export declare const CompactDailyOutputSchema: z.ZodObject<{
@@ -327,9 +364,12 @@ export declare const CompactDailyOutputSchema: z.ZodObject<{
327
364
  partition: "partition";
328
365
  "dismiss-filter": "dismiss-filter";
329
366
  "gist-checkpoint": "gist-checkpoint";
367
+ "gist-staleness": "gist-staleness";
330
368
  }>;
331
369
  operation: z.ZodString;
332
370
  message: z.ZodString;
371
+ timestamp: z.ZodOptional<z.ZodString>;
372
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
333
373
  }, z.core.$strip>>;
334
374
  }, z.core.$strip>;
335
375
  export declare const SearchOutputSchema: z.ZodObject<{
@@ -426,6 +466,14 @@ export declare const InitOutputSchema: z.ZodObject<{
426
466
  username: z.ZodString;
427
467
  message: z.ZodString;
428
468
  }, z.core.$strip>;
469
+ export declare const ManifestOutputSchema: z.ZodObject<{
470
+ schemaVersion: z.ZodLiteral<1>;
471
+ cliVersion: z.ZodString;
472
+ commands: z.ZodArray<z.ZodObject<{
473
+ name: z.ZodString;
474
+ localOnly: z.ZodBoolean;
475
+ }, z.core.$strip>>;
476
+ }, z.core.$strip>;
429
477
  export declare const CheckSetupOutputSchema: z.ZodObject<{
430
478
  setupComplete: z.ZodBoolean;
431
479
  username: z.ZodString;
@@ -791,6 +839,8 @@ export interface CommentsOutput {
791
839
  inlineCommentCount: number;
792
840
  discussionCommentCount: number;
793
841
  };
842
+ /** Non-fatal warnings (e.g. stale-cache fallback, #1193). Always present; empty on clean runs. */
843
+ warnings?: DailyWarning[];
794
844
  }
795
845
  /** Output of the post command */
796
846
  export interface PostOutput {
@@ -3,6 +3,18 @@
3
3
  * Provides structured output that can be consumed by scripts and plugins
4
4
  */
5
5
  import { z } from 'zod';
6
+ export function buildStalenessWarning(info) {
7
+ return {
8
+ phase: 'gist-staleness',
9
+ operation: 'state refresh',
10
+ message: `Operating on ${info.source} state — Gist refresh failed: ${info.reason}`,
11
+ timestamp: info.detectedAt,
12
+ details: {
13
+ source: info.source,
14
+ lastSuccessfulRefresh: info.lastSuccessfulRefresh,
15
+ },
16
+ };
17
+ }
6
18
  /**
7
19
  * Strip a full DailyOutput down to the compact subset (#763).
8
20
  * Omits summary, repoGroups, and full failures array. Retains a failureCount
@@ -61,6 +73,27 @@ export function compactRepoGroups(groups) {
61
73
  prUrls: group.prs.map((pr) => pr.url),
62
74
  }));
63
75
  }
76
+ // DailyWarning schema lives here (rather than further down with the rest of
77
+ // the daily schemas) so StatusOutputSchema below can reference it directly
78
+ // without `z.lazy()`. The runtime shape is shared across daily / status /
79
+ // comments warnings — see `DailyWarning` interface above.
80
+ const DailyWarningPhaseSchema = z.enum([
81
+ 'fetch',
82
+ 'repo-scores',
83
+ 'analytics',
84
+ 'scout-sync',
85
+ 'partition',
86
+ 'dismiss-filter',
87
+ 'gist-checkpoint',
88
+ 'gist-staleness',
89
+ ]);
90
+ const DailyWarningSchema = z.object({
91
+ phase: DailyWarningPhaseSchema,
92
+ operation: z.string(),
93
+ message: z.string(),
94
+ timestamp: z.string().optional(),
95
+ details: z.record(z.string(), z.unknown()).optional(),
96
+ });
64
97
  export const StatusOutputSchema = z.object({
65
98
  stats: z.object({
66
99
  mergedPRs: z.number().int().nonnegative(),
@@ -73,6 +106,7 @@ export const StatusOutputSchema = z.object({
73
106
  lastRunAt: z.string(),
74
107
  offline: z.boolean().optional(),
75
108
  lastUpdated: z.string().optional(),
109
+ warnings: z.array(DailyWarningSchema).optional(),
76
110
  });
77
111
  // ── Daily output schemas (#1146) ─────────────────────────────────────
78
112
  //
@@ -162,20 +196,8 @@ const CompactRepoGroupSchema = z.object({
162
196
  repo: z.string(),
163
197
  prUrls: z.array(z.string()),
164
198
  });
165
- const DailyWarningPhaseSchema = z.enum([
166
- 'fetch',
167
- 'repo-scores',
168
- 'analytics',
169
- 'scout-sync',
170
- 'partition',
171
- 'dismiss-filter',
172
- 'gist-checkpoint',
173
- ]);
174
- const DailyWarningSchema = z.object({
175
- phase: DailyWarningPhaseSchema,
176
- operation: z.string(),
177
- message: z.string(),
178
- });
199
+ // DailyWarning schemas were hoisted above StatusOutputSchema (#1193) so the
200
+ // status output can reference them without `z.lazy()`.
179
201
  export const DailyOutputSchema = z.object({
180
202
  digest: DailyDigestCompactSchema,
181
203
  capacity: CapacityAssessmentSchema,
@@ -278,6 +300,19 @@ export const InitOutputSchema = z.object({
278
300
  username: z.string(),
279
301
  message: z.string(),
280
302
  });
303
+ // ── #1190: plugin → CLI contract ─────────────────────────────────────
304
+ //
305
+ // Pinned shape lets the plugin's session-start hook verify that the bundled
306
+ // CLI exposes the subcommands the markdown layer expects. Bumping
307
+ // `schemaVersion` is a breaking change to that contract.
308
+ export const ManifestOutputSchema = z.object({
309
+ schemaVersion: z.literal(1),
310
+ cliVersion: z.string().regex(/^\d+\.\d+\.\d+/),
311
+ commands: z.array(z.object({
312
+ name: z.string(),
313
+ localOnly: z.boolean(),
314
+ })),
315
+ });
281
316
  export const CheckSetupOutputSchema = z.object({
282
317
  setupComplete: z.boolean(),
283
318
  username: z.string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,9 +54,9 @@
54
54
  "dependencies": {
55
55
  "@octokit/plugin-throttling": "^11.0.3",
56
56
  "@octokit/rest": "^22.0.1",
57
- "@oss-scout/core": "^0.7.1",
57
+ "@oss-scout/core": "^0.8.0",
58
58
  "commander": "^14.0.3",
59
- "zod": "^4.3.6"
59
+ "zod": "^4.4.3"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^25.6.0",