wicked-crew 0.7.25 → 0.7.26

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 (131) hide show
  1. package/dist/api/endpoint-manifest-live.d.ts.map +1 -1
  2. package/dist/api/endpoint-manifest-live.js +5 -0
  3. package/dist/api/endpoint-manifest-live.js.map +1 -1
  4. package/dist/api/eval-compare.d.ts +306 -0
  5. package/dist/api/eval-compare.d.ts.map +1 -0
  6. package/dist/api/eval-compare.js +610 -0
  7. package/dist/api/eval-compare.js.map +1 -0
  8. package/dist/api/eval-sample.d.ts +158 -0
  9. package/dist/api/eval-sample.d.ts.map +1 -0
  10. package/dist/api/eval-sample.js +88 -0
  11. package/dist/api/eval-sample.js.map +1 -0
  12. package/dist/api/routes.d.ts +5 -0
  13. package/dist/api/routes.d.ts.map +1 -1
  14. package/dist/api/routes.js +31 -0
  15. package/dist/api/routes.js.map +1 -1
  16. package/dist/api/run-files.d.ts +12 -2
  17. package/dist/api/run-files.d.ts.map +1 -1
  18. package/dist/api/run-files.js +17 -5
  19. package/dist/api/run-files.js.map +1 -1
  20. package/dist/api/server.d.ts +20 -0
  21. package/dist/api/server.d.ts.map +1 -1
  22. package/dist/api/server.js +62 -9
  23. package/dist/api/server.js.map +1 -1
  24. package/dist/api/skills.d.ts +88 -0
  25. package/dist/api/skills.d.ts.map +1 -0
  26. package/dist/api/skills.js +264 -0
  27. package/dist/api/skills.js.map +1 -0
  28. package/dist/api/testing.d.ts +2 -75
  29. package/dist/api/testing.d.ts.map +1 -1
  30. package/dist/api/testing.js +14 -28
  31. package/dist/api/testing.js.map +1 -1
  32. package/dist/core/adapter.d.ts +42 -0
  33. package/dist/core/adapter.d.ts.map +1 -1
  34. package/dist/core/adapter.js +83 -12
  35. package/dist/core/adapter.js.map +1 -1
  36. package/dist/interactive/bridge-root.d.ts +117 -7
  37. package/dist/interactive/bridge-root.d.ts.map +1 -1
  38. package/dist/interactive/bridge-root.js +229 -9
  39. package/dist/interactive/bridge-root.js.map +1 -1
  40. package/dist/interactive/doc-delete-routes.d.ts +2 -0
  41. package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
  42. package/dist/interactive/doc-delete-routes.js +3 -19
  43. package/dist/interactive/doc-delete-routes.js.map +1 -1
  44. package/dist/interactive/doc-list-routes.d.ts +42 -0
  45. package/dist/interactive/doc-list-routes.d.ts.map +1 -0
  46. package/dist/interactive/doc-list-routes.js +165 -0
  47. package/dist/interactive/doc-list-routes.js.map +1 -0
  48. package/dist/interactive/project-root.d.ts +36 -0
  49. package/dist/interactive/project-root.d.ts.map +1 -0
  50. package/dist/interactive/project-root.js +45 -0
  51. package/dist/interactive/project-root.js.map +1 -0
  52. package/dist/interactive/proxy-routes.d.ts +4 -1
  53. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  54. package/dist/interactive/proxy-routes.js +4 -22
  55. package/dist/interactive/proxy-routes.js.map +1 -1
  56. package/dist/projects/default-project.d.ts +10 -0
  57. package/dist/projects/default-project.d.ts.map +1 -0
  58. package/dist/projects/default-project.js +10 -0
  59. package/dist/projects/default-project.js.map +1 -0
  60. package/dist/projects/routes.d.ts +0 -2
  61. package/dist/projects/routes.d.ts.map +1 -1
  62. package/dist/projects/routes.js +1 -2
  63. package/dist/projects/routes.js.map +1 -1
  64. package/dist/skills/bundle.d.ts +94 -0
  65. package/dist/skills/bundle.d.ts.map +1 -0
  66. package/dist/skills/bundle.js +187 -0
  67. package/dist/skills/bundle.js.map +1 -0
  68. package/dist/skills/contain.d.ts +57 -0
  69. package/dist/skills/contain.d.ts.map +1 -0
  70. package/dist/skills/contain.js +90 -0
  71. package/dist/skills/contain.js.map +1 -0
  72. package/dist/skills/core-closure.d.ts +83 -0
  73. package/dist/skills/core-closure.d.ts.map +1 -0
  74. package/dist/skills/core-closure.js +130 -0
  75. package/dist/skills/core-closure.js.map +1 -0
  76. package/dist/skills/engine-env.d.ts +48 -0
  77. package/dist/skills/engine-env.d.ts.map +1 -0
  78. package/dist/skills/engine-env.js +69 -0
  79. package/dist/skills/engine-env.js.map +1 -0
  80. package/dist/skills/frontmatter.d.ts +77 -0
  81. package/dist/skills/frontmatter.d.ts.map +1 -0
  82. package/dist/skills/frontmatter.js +190 -0
  83. package/dist/skills/frontmatter.js.map +1 -0
  84. package/dist/skills/guards.d.ts +72 -0
  85. package/dist/skills/guards.d.ts.map +1 -0
  86. package/dist/skills/guards.js +147 -0
  87. package/dist/skills/guards.js.map +1 -0
  88. package/dist/skills/live-generations.d.ts +80 -0
  89. package/dist/skills/live-generations.d.ts.map +1 -0
  90. package/dist/skills/live-generations.js +163 -0
  91. package/dist/skills/live-generations.js.map +1 -0
  92. package/dist/skills/plugin-source.d.ts +109 -0
  93. package/dist/skills/plugin-source.d.ts.map +1 -0
  94. package/dist/skills/plugin-source.js +272 -0
  95. package/dist/skills/plugin-source.js.map +1 -0
  96. package/dist/skills/refs.d.ts +58 -0
  97. package/dist/skills/refs.d.ts.map +1 -0
  98. package/dist/skills/refs.js +133 -0
  99. package/dist/skills/refs.js.map +1 -0
  100. package/dist/skills/root-fence.d.ts +53 -0
  101. package/dist/skills/root-fence.d.ts.map +1 -0
  102. package/dist/skills/root-fence.js +109 -0
  103. package/dist/skills/root-fence.js.map +1 -0
  104. package/dist/skills/root-names.d.ts +42 -0
  105. package/dist/skills/root-names.d.ts.map +1 -0
  106. package/dist/skills/root-names.js +44 -0
  107. package/dist/skills/root-names.js.map +1 -0
  108. package/dist/skills/runtime.d.ts +130 -0
  109. package/dist/skills/runtime.d.ts.map +1 -0
  110. package/dist/skills/runtime.js +246 -0
  111. package/dist/skills/runtime.js.map +1 -0
  112. package/dist/skills/store.d.ts +895 -0
  113. package/dist/skills/store.d.ts.map +1 -0
  114. package/dist/skills/store.js +3575 -0
  115. package/dist/skills/store.js.map +1 -0
  116. package/dist/skills/tree.d.ts +292 -0
  117. package/dist/skills/tree.d.ts.map +1 -0
  118. package/dist/skills/tree.js +645 -0
  119. package/dist/skills/tree.js.map +1 -0
  120. package/dist/skills/venv.d.ts +52 -0
  121. package/dist/skills/venv.d.ts.map +1 -0
  122. package/dist/skills/venv.js +80 -0
  123. package/dist/skills/venv.js.map +1 -0
  124. package/dist/studio/assets/index-CK1uJWs2.css +32 -0
  125. package/dist/studio/assets/index-FcZg3WDR.js +539 -0
  126. package/dist/studio/index.html +2 -2
  127. package/dist/studio/testid-inventory.json +605 -7
  128. package/endpoint-manifest.json +183 -1
  129. package/package.json +4 -3
  130. package/dist/studio/assets/index-C4iz4uAw.js +0 -537
  131. package/dist/studio/assets/index-DEEQcRQ_.css +0 -32
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Which published generations a LIVE run may still be reading (design v3 DECISION 1: "old
3
+ * generations are reaped only after no live session references them").
4
+ *
5
+ * The engine resolves `WICKED_SKILLS_SNAPSHOT` at every worker spawn and reports the generation it
6
+ * ADMITTED as a `skillsSnapshotHanded` CoreEvent ({session, gen, …}) — the authoritative record of
7
+ * which generation each session's spawn was actually handed. Crew keeps TWO kinds of pin:
8
+ *
9
+ * session pins a session is tracked from its first event; a `skillsSnapshotHanded` pins the
10
+ * EXACT generation the engine reports (its `gen` field), never "whatever `current`
11
+ * happens to be when crew observes some later event" (codex round 3); every
12
+ * generation published WHILE the session is live is pinned to it too — its next
13
+ * unit spawn reads (and will report) the new `current`; the terminal frame
14
+ * (`sessionCompleted` / `sessionFailed` / `runCancelled` — the same triple
15
+ * api/seat-health.ts drops assignments on) releases every pin the session held.
16
+ *
17
+ * launch pins EXPLICIT ACCOUNTING for the gap between a launch and the engine's report. When
18
+ * the daemon hands a launch to the engine (`CoreAdapter.onLaunch`: launchRun,
19
+ * resumeRun, confirmGate, launchCampaign, resumeCampaign — every path a spawn can
20
+ * come from; the campaign's own DAG-node runs are pinned as the engine announces
21
+ * them, `campaignNodeStarted`, under the campaign's umbrella pin), crew opens a pin
22
+ * for that launch holding the generation the env exports RIGHT NOW, and every
23
+ * generation published after that is added to it (the spawn may read any of them).
24
+ * A launch pin is released ONLY when the engine reports the handed generation for
25
+ * that session (`skillsSnapshotHanded` — the session pin takes over), when the
26
+ * run/campaign reaches a terminal state, or when the engine REJECTED the launch
27
+ * (nothing will ever read it). It never expires by publish count or age (codex
28
+ * round 4: a bounded window let a run's generation be reaped while its handoff
29
+ * report was still outstanding — publish count bounds nothing about handoff delay).
30
+ * Reaping skips any generation with an unreleased launch pin.
31
+ *
32
+ * The ledger is in-memory: the engine is in-process, so a daemon restart ends every worker it
33
+ * spawned, and a resumed run's launch (`resumeRun`) opens a fresh launch pin.
34
+ */
35
+ import type { CoreEvent } from '../core/types.js';
36
+ export type LiveGenerationObservation = 'pinned' | 'released' | 'ignored';
37
+ /** What the daemon hands the engine: a run (launch / resume / gate answer) or a campaign. */
38
+ export type LaunchKind = 'run' | 'campaign';
39
+ export declare class LiveGenerations {
40
+ /** Session pins: session id → the generations the engine reported + every one published since. */
41
+ private readonly sessions;
42
+ /** Launch pins: `run:<id>` / `campaign:<id>` → the generations the launch may read, until it is accounted for. */
43
+ private readonly launchPins;
44
+ /** The generation the env exports right now (`exported`), or `null` when none is (fallback / unpublished). */
45
+ private exportedGen;
46
+ private static key;
47
+ /**
48
+ * Fold one CoreEvent: track a live session, pin the EXACT generation the engine reports on a
49
+ * `skillsSnapshotHanded` (releasing that session's launch pin — the engine has accounted for the
50
+ * launch), release on a terminal frame; pin a campaign's node run as the engine announces it and
51
+ * release the campaign's pin on its terminal frame. A non-`skillsSnapshotHanded` session event
52
+ * only tracks the session — it never pins "current at event time" (the bug codex round 3
53
+ * replaced): the generation a spawn actually used comes from the engine's own report, and the
54
+ * pre-report window is covered by the launch pin.
55
+ */
56
+ observe(event: CoreEvent): LiveGenerationObservation;
57
+ /**
58
+ * The env now exports generation `gen` to new launches (`WICKED_SKILLS_SNAPSHOT`;
59
+ * runtime.afterPublish), or nothing (`null`). A launch pin opened after this starts from it.
60
+ */
61
+ exported(gen: number | null): void;
62
+ /**
63
+ * The daemon is handing a launch to the engine (before the engine call, so no spawn can read the
64
+ * env ahead of the pin): open its launch pin at the generation the env exports right now. The pin
65
+ * accumulates every later publish (`published`) and is released only by the engine's report for
66
+ * that session, the run's / campaign's terminal frame, or `launchRejected`.
67
+ */
68
+ launched(kind: LaunchKind, id: string): void;
69
+ /** The engine REFUSED the launch (the call rejected): nothing will read it — release its pin. */
70
+ launchRejected(kind: LaunchKind, id: string): void;
71
+ /** A generation was just published: every live session and every open launch may read it at its next spawn. */
72
+ published(gen: number): void;
73
+ /** The union of every live session's pins and every open launch pin — what the reaper must keep. */
74
+ pinned(): ReadonlySet<number>;
75
+ /** Live sessions being tracked (diagnostics + tests). */
76
+ liveSessions(): string[];
77
+ /** Launches handed to the engine and not yet accounted for (`run:<id>` / `campaign:<id>`; diagnostics + tests). */
78
+ openLaunches(): string[];
79
+ }
80
+ //# sourceMappingURL=live-generations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-generations.d.ts","sourceRoot":"","sources":["../../src/skills/live-generations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAclD,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1E,6FAA6F;AAC7F,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,UAAU,CAAC;AAe5C,qBAAa,eAAe;IAC1B,kGAAkG;IAClG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,kHAAkH;IAClH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,8GAA8G;IAC9G,OAAO,CAAC,WAAW,CAAuB;IAE1C,OAAO,CAAC,MAAM,CAAC,GAAG;IAIlB;;;;;;;;OAQG;IACH,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,yBAAyB;IAoCpD;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIlC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAO5C,iGAAiG;IACjG,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAIlD,+GAA+G;IAC/G,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAK5B,oGAAoG;IACpG,MAAM,IAAI,WAAW,CAAC,MAAM,CAAC;IAO7B,yDAAyD;IACzD,YAAY,IAAI,MAAM,EAAE;IAIxB,mHAAmH;IACnH,YAAY,IAAI,MAAM,EAAE;CAGzB"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Which published generations a LIVE run may still be reading (design v3 DECISION 1: "old
3
+ * generations are reaped only after no live session references them").
4
+ *
5
+ * The engine resolves `WICKED_SKILLS_SNAPSHOT` at every worker spawn and reports the generation it
6
+ * ADMITTED as a `skillsSnapshotHanded` CoreEvent ({session, gen, …}) — the authoritative record of
7
+ * which generation each session's spawn was actually handed. Crew keeps TWO kinds of pin:
8
+ *
9
+ * session pins a session is tracked from its first event; a `skillsSnapshotHanded` pins the
10
+ * EXACT generation the engine reports (its `gen` field), never "whatever `current`
11
+ * happens to be when crew observes some later event" (codex round 3); every
12
+ * generation published WHILE the session is live is pinned to it too — its next
13
+ * unit spawn reads (and will report) the new `current`; the terminal frame
14
+ * (`sessionCompleted` / `sessionFailed` / `runCancelled` — the same triple
15
+ * api/seat-health.ts drops assignments on) releases every pin the session held.
16
+ *
17
+ * launch pins EXPLICIT ACCOUNTING for the gap between a launch and the engine's report. When
18
+ * the daemon hands a launch to the engine (`CoreAdapter.onLaunch`: launchRun,
19
+ * resumeRun, confirmGate, launchCampaign, resumeCampaign — every path a spawn can
20
+ * come from; the campaign's own DAG-node runs are pinned as the engine announces
21
+ * them, `campaignNodeStarted`, under the campaign's umbrella pin), crew opens a pin
22
+ * for that launch holding the generation the env exports RIGHT NOW, and every
23
+ * generation published after that is added to it (the spawn may read any of them).
24
+ * A launch pin is released ONLY when the engine reports the handed generation for
25
+ * that session (`skillsSnapshotHanded` — the session pin takes over), when the
26
+ * run/campaign reaches a terminal state, or when the engine REJECTED the launch
27
+ * (nothing will ever read it). It never expires by publish count or age (codex
28
+ * round 4: a bounded window let a run's generation be reaped while its handoff
29
+ * report was still outstanding — publish count bounds nothing about handoff delay).
30
+ * Reaping skips any generation with an unreleased launch pin.
31
+ *
32
+ * The ledger is in-memory: the engine is in-process, so a daemon restart ends every worker it
33
+ * spawned, and a resumed run's launch (`resumeRun`) opens a fresh launch pin.
34
+ */
35
+ /** The frames after which a session spawns nothing more. */
36
+ const TERMINAL_EVENT_TYPES = new Set(['sessionCompleted', 'sessionFailed', 'runCancelled']);
37
+ /** The frames after which a campaign launches no more node runs. */
38
+ const CAMPAIGN_TERMINAL_EVENT_TYPES = new Set(['campaignCompleted', 'campaignFailed', 'campaignCancelled']);
39
+ /** The engine's per-spawn record of the generation it handed a session. */
40
+ const SKILLS_SNAPSHOT_HANDED = 'skillsSnapshotHanded';
41
+ /** The engine's announcement of a campaign DAG-node run: `{campaign, node, runId}`. */
42
+ const CAMPAIGN_NODE_STARTED = 'campaignNodeStarted';
43
+ /** The generation a `skillsSnapshotHanded` reports, or `null` (a live-cache fallback carries no
44
+ * reapable generation — `gen: null`). */
45
+ function handedGeneration(event) {
46
+ const gen = event.gen;
47
+ if (typeof gen !== 'string' || !/^\d+$/.test(gen))
48
+ return null;
49
+ const n = Number(gen);
50
+ return Number.isInteger(n) && n >= 1 ? n : null;
51
+ }
52
+ function nonEmpty(value) {
53
+ return typeof value === 'string' && value !== '' ? value : undefined;
54
+ }
55
+ export class LiveGenerations {
56
+ /** Session pins: session id → the generations the engine reported + every one published since. */
57
+ sessions = new Map();
58
+ /** Launch pins: `run:<id>` / `campaign:<id>` → the generations the launch may read, until it is accounted for. */
59
+ launchPins = new Map();
60
+ /** The generation the env exports right now (`exported`), or `null` when none is (fallback / unpublished). */
61
+ exportedGen = null;
62
+ static key(kind, id) {
63
+ return `${kind}:${id}`;
64
+ }
65
+ /**
66
+ * Fold one CoreEvent: track a live session, pin the EXACT generation the engine reports on a
67
+ * `skillsSnapshotHanded` (releasing that session's launch pin — the engine has accounted for the
68
+ * launch), release on a terminal frame; pin a campaign's node run as the engine announces it and
69
+ * release the campaign's pin on its terminal frame. A non-`skillsSnapshotHanded` session event
70
+ * only tracks the session — it never pins "current at event time" (the bug codex round 3
71
+ * replaced): the generation a spawn actually used comes from the engine's own report, and the
72
+ * pre-report window is covered by the launch pin.
73
+ */
74
+ observe(event) {
75
+ const campaign = nonEmpty(event.campaign);
76
+ if (campaign !== undefined && event.session === undefined) {
77
+ if (CAMPAIGN_TERMINAL_EVENT_TYPES.has(event.type)) {
78
+ return this.launchPins.delete(LiveGenerations.key('campaign', campaign)) ? 'released' : 'ignored';
79
+ }
80
+ if (event.type === CAMPAIGN_NODE_STARTED) {
81
+ const runId = nonEmpty(event.runId);
82
+ if (runId === undefined)
83
+ return 'ignored';
84
+ this.launched('run', runId);
85
+ return 'pinned';
86
+ }
87
+ return 'ignored';
88
+ }
89
+ const session = nonEmpty(event.session);
90
+ if (session === undefined)
91
+ return 'ignored';
92
+ if (TERMINAL_EVENT_TYPES.has(event.type)) {
93
+ const hadSession = this.sessions.delete(session);
94
+ const hadLaunch = this.launchPins.delete(LiveGenerations.key('run', session));
95
+ return hadSession || hadLaunch ? 'released' : 'ignored';
96
+ }
97
+ let gens = this.sessions.get(session);
98
+ if (gens === undefined) {
99
+ gens = new Set();
100
+ this.sessions.set(session, gens);
101
+ }
102
+ if (event.type === SKILLS_SNAPSHOT_HANDED) {
103
+ const gen = handedGeneration(event);
104
+ if (gen !== null)
105
+ gens.add(gen);
106
+ // The engine has accounted for this launch — which generation it used (or that it used
107
+ // none: a live-cache fallback). The durable session pin lives until the terminal frame.
108
+ this.launchPins.delete(LiveGenerations.key('run', session));
109
+ }
110
+ return 'pinned';
111
+ }
112
+ /**
113
+ * The env now exports generation `gen` to new launches (`WICKED_SKILLS_SNAPSHOT`;
114
+ * runtime.afterPublish), or nothing (`null`). A launch pin opened after this starts from it.
115
+ */
116
+ exported(gen) {
117
+ this.exportedGen = gen;
118
+ }
119
+ /**
120
+ * The daemon is handing a launch to the engine (before the engine call, so no spawn can read the
121
+ * env ahead of the pin): open its launch pin at the generation the env exports right now. The pin
122
+ * accumulates every later publish (`published`) and is released only by the engine's report for
123
+ * that session, the run's / campaign's terminal frame, or `launchRejected`.
124
+ */
125
+ launched(kind, id) {
126
+ const key = LiveGenerations.key(kind, id);
127
+ const gens = this.launchPins.get(key) ?? new Set();
128
+ if (this.exportedGen !== null)
129
+ gens.add(this.exportedGen);
130
+ this.launchPins.set(key, gens);
131
+ }
132
+ /** The engine REFUSED the launch (the call rejected): nothing will read it — release its pin. */
133
+ launchRejected(kind, id) {
134
+ this.launchPins.delete(LiveGenerations.key(kind, id));
135
+ }
136
+ /** A generation was just published: every live session and every open launch may read it at its next spawn. */
137
+ published(gen) {
138
+ for (const gens of this.sessions.values())
139
+ gens.add(gen);
140
+ for (const gens of this.launchPins.values())
141
+ gens.add(gen);
142
+ }
143
+ /** The union of every live session's pins and every open launch pin — what the reaper must keep. */
144
+ pinned() {
145
+ const out = new Set();
146
+ for (const gens of this.sessions.values())
147
+ for (const gen of gens)
148
+ out.add(gen);
149
+ for (const gens of this.launchPins.values())
150
+ for (const gen of gens)
151
+ out.add(gen);
152
+ return out;
153
+ }
154
+ /** Live sessions being tracked (diagnostics + tests). */
155
+ liveSessions() {
156
+ return [...this.sessions.keys()].sort();
157
+ }
158
+ /** Launches handed to the engine and not yet accounted for (`run:<id>` / `campaign:<id>`; diagnostics + tests). */
159
+ openLaunches() {
160
+ return [...this.launchPins.keys()].sort();
161
+ }
162
+ }
163
+ //# sourceMappingURL=live-generations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-generations.js","sourceRoot":"","sources":["../../src/skills/live-generations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Where the shipped baseline comes from: the LIVE installed wicked-garden plugin.
3
+ *
4
+ * The daemon seeds (and refreshes) its skills root from the plugin Claude Code actually runs —
5
+ * the marketplace cache under `<CLAUDE_CONFIG_DIR|~/.claude>/plugins/cache/wicked-garden/
6
+ * wicked-garden/<version>` (highest version wins). That is the ONLY automatic source (design v3
7
+ * §3/§8; codex review of #480): the hand-installed `<config dir>/plugins/wicked-garden` copy is the
8
+ * exact stale artifact the operator's `clis.toml` hack pointed workers at (12.28.1 while the live
9
+ * cache was 12.32.0 — design v3 §"Verified mechanics"), so it is never a fallback. NEVER a repo
10
+ * checkout by default either. `WICKED_CREW_SKILLS_SOURCE` is the one explicit override — for
11
+ * tests, and for an operator who deliberately wants a checkout or that hand copy. A machine without
12
+ * the cache has no source at all: crew does not vendor garden — the seed says "install garden
13
+ * first" loudly (`SkillsSourceUnavailableError`) and the runtime leaves the engine input unset.
14
+ *
15
+ * # No-follow BELOW the root (codex round 6 on #480)
16
+ *
17
+ * The source ROOT may legitimately be reached through a symlink — operators symlink `~/.claude`,
18
+ * and the marketplace cache may sit behind a linked config dir — so the root is resolved ONCE
19
+ * (`realPluginRoot`, the one place a link is followed) and every designated entry BELOW it is
20
+ * lstat-walked (`noFollowEntry`): the manifest dir, `plugin.json`, each catalog, each root file,
21
+ * each bundle directory and everything inside it. A symlink among them is refused by name
22
+ * (`PluginSourceSymlinkError`) — a seed refuses to start (the runtime's `skills.config`), a
23
+ * refresh answers a `blocked` envelope — and nothing is copied: a linked catalog or a linked skill
24
+ * dir would otherwise copy bytes from outside the declared source into the daemon's root and hand
25
+ * them to every worker.
26
+ */
27
+ import type { SkillSourceKind } from '../core/types.js';
28
+ /** Explicit plugin-source override — the seam tests and proof scripts aim at a fixture plugin. */
29
+ export declare const SKILLS_SOURCE_ENV = "WICKED_CREW_SKILLS_SOURCE";
30
+ /** Relative path of the Claude Code plugin manifest inside a plugin root. */
31
+ export declare const PLUGIN_MANIFEST_REL: string;
32
+ export interface PluginSource {
33
+ /** Absolute plugin root (the directory holding `.claude-plugin/plugin.json`). */
34
+ path: string;
35
+ kind: SkillSourceKind;
36
+ /** `version` from the plugin manifest. */
37
+ plugin_version: string;
38
+ }
39
+ /** The Claude Code config dir the plugin cache lives under: `CLAUDE_CONFIG_DIR`, else `~/.claude`. */
40
+ export declare function claudeConfigDir(env?: NodeJS.ProcessEnv, home?: string): string;
41
+ /**
42
+ * A designated entry of the plugin source is a symlink — the seed/refresh is refused by name and
43
+ * nothing is copied (module header). `entry` is the plugin-relative POSIX path of the link.
44
+ */
45
+ export declare class PluginSourceSymlinkError extends Error {
46
+ readonly root: string;
47
+ readonly entry: string;
48
+ readonly target: string;
49
+ constructor(root: string, entry: string, target: string);
50
+ }
51
+ /**
52
+ * The plugin root with every link resolved — the ONE place a link is followed (an operator's
53
+ * symlinked config dir reaches the root). `null` when it does not exist or is not a directory.
54
+ */
55
+ export declare function realPluginRoot(dir: string): string | null;
56
+ /**
57
+ * `join(root, ...segments)` after lstat-walking every segment BELOW the canonical root: a symlink at
58
+ * any of them throws `PluginSourceSymlinkError` naming it; a component that does not exist (or sits
59
+ * below a regular file) answers `null`. `sourceSpelling` is the root as the operator spelled it, for
60
+ * the error.
61
+ */
62
+ export declare function noFollowEntry(root: string, segments: ReadonlyArray<string>, sourceSpelling?: string): string | null;
63
+ /**
64
+ * The plugin manifest's `version` at `dir`, or `null` when `dir` is not a plugin root. The manifest
65
+ * is read NO-FOLLOW below the (once-resolved) root: a symlinked `.claude-plugin/` or `plugin.json`
66
+ * throws `PluginSourceSymlinkError` — never a version read through a link (codex round 6).
67
+ */
68
+ export declare function pluginVersionAt(dir: string): string | null;
69
+ /**
70
+ * A TOTAL order over version strings, so "highest version wins" never depends on `readdir` order
71
+ * (Copilot on #480). Numeric dotted order first (`12.9.0` < `12.32.0`; a shorter release sorts
72
+ * before a longer one with the same prefix; a non-integer segment sorts below any integer). A
73
+ * prerelease suffix (`12.0.0-beta`, everything after the first `-`) sorts BELOW its release
74
+ * (`12.0.0-beta` < `12.0.0`), and two prereleases compare identifier by identifier the semver way
75
+ * (numeric identifiers numerically and before alphanumeric ones, then lexically, shorter first).
76
+ * Distinct strings NEVER compare equal: whatever survives all of that (`12.00.0` vs `12.0.0`) is
77
+ * ordered lexically.
78
+ */
79
+ export declare function compareVersions(a: string, b: string): number;
80
+ /**
81
+ * Classify a plugin root: a git checkout when it carries `.git`; the installed plugin when it
82
+ * sits under a `plugins/` directory (the marketplace cache or the hand-installed copy); anything
83
+ * else is an explicit plugin-shaped directory (an unpacked tarball, a test fixture).
84
+ */
85
+ export declare function classifySource(dir: string): SkillSourceKind;
86
+ /** A `PluginSource` for an explicit plugin root, or `null` when it is not a plugin root. */
87
+ export declare function pluginSourceAt(dir: string): PluginSource | null;
88
+ /** The marketplace cache dir the live plugin versions live under, for a config dir. */
89
+ export declare function livePluginCacheDir(configDir: string): string;
90
+ /**
91
+ * Discover the live installed plugin: the explicit `WICKED_CREW_SKILLS_SOURCE` override, else the
92
+ * highest version in the marketplace cache. NOTHING else — in particular not the hand-installed
93
+ * `plugins/wicked-garden` copy (see the module header). `env`/`home` are injectable so tests never
94
+ * read the developer's real config dir. Returns `null` when nothing is installed — the caller says
95
+ * "install garden first" loudly.
96
+ */
97
+ export declare function discoverLivePlugin(opts?: {
98
+ env?: NodeJS.ProcessEnv;
99
+ home?: string;
100
+ }): PluginSource | null;
101
+ export interface GitState {
102
+ git_sha: string | null;
103
+ git_dirty: boolean;
104
+ /** Present when the source is a checkout but git could not answer — the caller logs it. */
105
+ error?: string;
106
+ }
107
+ /** The checkout's HEAD sha + dirty flag; `{null,false}` for a non-checkout source. */
108
+ export declare function gitStateOf(source: PluginSource): GitState;
109
+ //# sourceMappingURL=plugin-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-source.d.ts","sourceRoot":"","sources":["../../src/skills/plugin-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAOH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIxD,kGAAkG;AAClG,eAAO,MAAM,iBAAiB,8BAA8B,CAAC;AAE7D,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,QAAwC,CAAC;AAEzE,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,eAAe,CAAC;IACtB,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAGtG;AAED;;;GAGG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IAE/C,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM;CAS1B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,GAAE,MAAa,GAAG,MAAM,GAAG,IAAI,CAezH;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAa5D;AA+CD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAI3D;AAED,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAG/D;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,YAAY,GAAG,IAAI,CAerB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,QAAQ,CAuBzD"}
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Where the shipped baseline comes from: the LIVE installed wicked-garden plugin.
3
+ *
4
+ * The daemon seeds (and refreshes) its skills root from the plugin Claude Code actually runs —
5
+ * the marketplace cache under `<CLAUDE_CONFIG_DIR|~/.claude>/plugins/cache/wicked-garden/
6
+ * wicked-garden/<version>` (highest version wins). That is the ONLY automatic source (design v3
7
+ * §3/§8; codex review of #480): the hand-installed `<config dir>/plugins/wicked-garden` copy is the
8
+ * exact stale artifact the operator's `clis.toml` hack pointed workers at (12.28.1 while the live
9
+ * cache was 12.32.0 — design v3 §"Verified mechanics"), so it is never a fallback. NEVER a repo
10
+ * checkout by default either. `WICKED_CREW_SKILLS_SOURCE` is the one explicit override — for
11
+ * tests, and for an operator who deliberately wants a checkout or that hand copy. A machine without
12
+ * the cache has no source at all: crew does not vendor garden — the seed says "install garden
13
+ * first" loudly (`SkillsSourceUnavailableError`) and the runtime leaves the engine input unset.
14
+ *
15
+ * # No-follow BELOW the root (codex round 6 on #480)
16
+ *
17
+ * The source ROOT may legitimately be reached through a symlink — operators symlink `~/.claude`,
18
+ * and the marketplace cache may sit behind a linked config dir — so the root is resolved ONCE
19
+ * (`realPluginRoot`, the one place a link is followed) and every designated entry BELOW it is
20
+ * lstat-walked (`noFollowEntry`): the manifest dir, `plugin.json`, each catalog, each root file,
21
+ * each bundle directory and everything inside it. A symlink among them is refused by name
22
+ * (`PluginSourceSymlinkError`) — a seed refuses to start (the runtime's `skills.config`), a
23
+ * refresh answers a `blocked` envelope — and nothing is copied: a linked catalog or a linked skill
24
+ * dir would otherwise copy bytes from outside the declared source into the daemon's root and hand
25
+ * them to every worker.
26
+ */
27
+ import { execFileSync } from 'node:child_process';
28
+ import { existsSync, lstatSync, readdirSync, readlinkSync, realpathSync } from 'node:fs';
29
+ import { homedir } from 'node:os';
30
+ import { join, sep } from 'node:path';
31
+ import { PLUGIN_NAME } from './frontmatter.js';
32
+ import { readFileNoFollow } from './tree.js';
33
+ /** Explicit plugin-source override — the seam tests and proof scripts aim at a fixture plugin. */
34
+ export const SKILLS_SOURCE_ENV = 'WICKED_CREW_SKILLS_SOURCE';
35
+ /** Relative path of the Claude Code plugin manifest inside a plugin root. */
36
+ export const PLUGIN_MANIFEST_REL = join('.claude-plugin', 'plugin.json');
37
+ /** The Claude Code config dir the plugin cache lives under: `CLAUDE_CONFIG_DIR`, else `~/.claude`. */
38
+ export function claudeConfigDir(env = process.env, home = homedir()) {
39
+ const configured = env['CLAUDE_CONFIG_DIR'];
40
+ return configured !== undefined && configured !== '' ? configured : join(home, '.claude');
41
+ }
42
+ /**
43
+ * A designated entry of the plugin source is a symlink — the seed/refresh is refused by name and
44
+ * nothing is copied (module header). `entry` is the plugin-relative POSIX path of the link.
45
+ */
46
+ export class PluginSourceSymlinkError extends Error {
47
+ root;
48
+ entry;
49
+ target;
50
+ constructor(root, entry, target) {
51
+ super(`plugin source ${root}: ${entry} is a symlink (-> ${target}) — the plugin source is copied into the daemon's skills root and ` +
52
+ 'handed to every worker, so a link among its designated files or directories would copy whatever it reaches; ' +
53
+ 'refused, nothing was copied (the source root itself may be reached through a link; its contents may not)');
54
+ this.root = root;
55
+ this.entry = entry;
56
+ this.target = target;
57
+ this.name = 'PluginSourceSymlinkError';
58
+ }
59
+ }
60
+ /**
61
+ * The plugin root with every link resolved — the ONE place a link is followed (an operator's
62
+ * symlinked config dir reaches the root). `null` when it does not exist or is not a directory.
63
+ */
64
+ export function realPluginRoot(dir) {
65
+ let real;
66
+ try {
67
+ real = realpathSync(dir);
68
+ }
69
+ catch (err) {
70
+ if (err.code === 'ENOENT' || err.code === 'ENOTDIR')
71
+ return null;
72
+ throw err;
73
+ }
74
+ return lstatSync(real).isDirectory() ? real : null;
75
+ }
76
+ /**
77
+ * `join(root, ...segments)` after lstat-walking every segment BELOW the canonical root: a symlink at
78
+ * any of them throws `PluginSourceSymlinkError` naming it; a component that does not exist (or sits
79
+ * below a regular file) answers `null`. `sourceSpelling` is the root as the operator spelled it, for
80
+ * the error.
81
+ */
82
+ export function noFollowEntry(root, segments, sourceSpelling = root) {
83
+ let cur = root;
84
+ for (let i = 0; i < segments.length; i += 1) {
85
+ cur = join(cur, segments[i]);
86
+ let st;
87
+ try {
88
+ st = lstatSync(cur);
89
+ }
90
+ catch (err) {
91
+ const code = err.code;
92
+ if (code === 'ENOENT' || code === 'ENOTDIR')
93
+ return null;
94
+ throw err;
95
+ }
96
+ if (st.isSymbolicLink())
97
+ throw new PluginSourceSymlinkError(sourceSpelling, segments.slice(0, i + 1).join('/'), readlinkSync(cur));
98
+ }
99
+ return cur;
100
+ }
101
+ /**
102
+ * The plugin manifest's `version` at `dir`, or `null` when `dir` is not a plugin root. The manifest
103
+ * is read NO-FOLLOW below the (once-resolved) root: a symlinked `.claude-plugin/` or `plugin.json`
104
+ * throws `PluginSourceSymlinkError` — never a version read through a link (codex round 6).
105
+ */
106
+ export function pluginVersionAt(dir) {
107
+ const root = realPluginRoot(dir);
108
+ if (root === null)
109
+ return null;
110
+ const manifest = noFollowEntry(root, PLUGIN_MANIFEST_REL.split(sep), dir);
111
+ if (manifest === null || !lstatSync(manifest).isFile())
112
+ return null;
113
+ const parsed = JSON.parse(readFileNoFollow(manifest).toString('utf8')); // the entry the walk judged is the one read (v3.5 §3)
114
+ if (typeof parsed !== 'object' || parsed === null)
115
+ return null;
116
+ const version = parsed.version;
117
+ return typeof version === 'string' && version !== '' ? version : null;
118
+ }
119
+ /**
120
+ * A TOTAL order over version strings, so "highest version wins" never depends on `readdir` order
121
+ * (Copilot on #480). Numeric dotted order first (`12.9.0` < `12.32.0`; a shorter release sorts
122
+ * before a longer one with the same prefix; a non-integer segment sorts below any integer). A
123
+ * prerelease suffix (`12.0.0-beta`, everything after the first `-`) sorts BELOW its release
124
+ * (`12.0.0-beta` < `12.0.0`), and two prereleases compare identifier by identifier the semver way
125
+ * (numeric identifiers numerically and before alphanumeric ones, then lexically, shorter first).
126
+ * Distinct strings NEVER compare equal: whatever survives all of that (`12.00.0` vs `12.0.0`) is
127
+ * ordered lexically.
128
+ */
129
+ export function compareVersions(a, b) {
130
+ if (a === b)
131
+ return 0;
132
+ const [coreA, preA] = splitPrerelease(a);
133
+ const [coreB, preB] = splitPrerelease(b);
134
+ const core = compareDotted(coreA, coreB);
135
+ if (core !== 0)
136
+ return core;
137
+ if (preA === null && preB !== null)
138
+ return 1;
139
+ if (preA !== null && preB === null)
140
+ return -1;
141
+ if (preA !== null && preB !== null) {
142
+ const pre = comparePrerelease(preA, preB);
143
+ if (pre !== 0)
144
+ return pre;
145
+ }
146
+ return a < b ? -1 : 1;
147
+ }
148
+ /** `12.0.0-beta.1` → [`12.0.0`, `beta.1`]; no `-` → [`v`, null]. */
149
+ function splitPrerelease(v) {
150
+ const dash = v.indexOf('-');
151
+ return dash === -1 ? [v, null] : [v.slice(0, dash), v.slice(dash + 1)];
152
+ }
153
+ /** Dotted numeric segments: shorter-first on a shared prefix, a non-integer segment below any integer. */
154
+ function compareDotted(a, b) {
155
+ const pa = a.split('.');
156
+ const pb = b.split('.');
157
+ const n = Math.max(pa.length, pb.length);
158
+ for (let i = 0; i < n; i += 1) {
159
+ const sa = pa[i];
160
+ const sb = pb[i];
161
+ if (sa === undefined)
162
+ return -1;
163
+ if (sb === undefined)
164
+ return 1;
165
+ const na = /^\d+$/.test(sa) ? Number(sa) : -1;
166
+ const nb = /^\d+$/.test(sb) ? Number(sb) : -1;
167
+ if (na !== nb)
168
+ return na < nb ? -1 : 1;
169
+ }
170
+ return 0;
171
+ }
172
+ /** Semver prerelease identifiers: numeric before alphanumeric, numeric numerically, else lexically; shorter first. */
173
+ function comparePrerelease(a, b) {
174
+ const pa = a.split('.');
175
+ const pb = b.split('.');
176
+ const n = Math.max(pa.length, pb.length);
177
+ for (let i = 0; i < n; i += 1) {
178
+ const sa = pa[i];
179
+ const sb = pb[i];
180
+ if (sa === undefined)
181
+ return -1;
182
+ if (sb === undefined)
183
+ return 1;
184
+ const numA = /^\d+$/.test(sa);
185
+ const numB = /^\d+$/.test(sb);
186
+ if (numA && numB) {
187
+ if (Number(sa) !== Number(sb))
188
+ return Number(sa) < Number(sb) ? -1 : 1;
189
+ continue;
190
+ }
191
+ if (numA !== numB)
192
+ return numA ? -1 : 1;
193
+ if (sa !== sb)
194
+ return sa < sb ? -1 : 1;
195
+ }
196
+ return 0;
197
+ }
198
+ /**
199
+ * Classify a plugin root: a git checkout when it carries `.git`; the installed plugin when it
200
+ * sits under a `plugins/` directory (the marketplace cache or the hand-installed copy); anything
201
+ * else is an explicit plugin-shaped directory (an unpacked tarball, a test fixture).
202
+ */
203
+ export function classifySource(dir) {
204
+ if (existsSync(join(dir, '.git')))
205
+ return 'checkout';
206
+ if (dir.split(sep).includes('plugins'))
207
+ return 'claude-plugin-cache';
208
+ return 'directory';
209
+ }
210
+ /** A `PluginSource` for an explicit plugin root, or `null` when it is not a plugin root. */
211
+ export function pluginSourceAt(dir) {
212
+ const version = pluginVersionAt(dir);
213
+ return version === null ? null : { path: dir, kind: classifySource(dir), plugin_version: version };
214
+ }
215
+ /** The marketplace cache dir the live plugin versions live under, for a config dir. */
216
+ export function livePluginCacheDir(configDir) {
217
+ return join(configDir, 'plugins', 'cache', PLUGIN_NAME, PLUGIN_NAME);
218
+ }
219
+ /**
220
+ * Discover the live installed plugin: the explicit `WICKED_CREW_SKILLS_SOURCE` override, else the
221
+ * highest version in the marketplace cache. NOTHING else — in particular not the hand-installed
222
+ * `plugins/wicked-garden` copy (see the module header). `env`/`home` are injectable so tests never
223
+ * read the developer's real config dir. Returns `null` when nothing is installed — the caller says
224
+ * "install garden first" loudly.
225
+ */
226
+ export function discoverLivePlugin(opts = {}) {
227
+ const env = opts.env ?? process.env;
228
+ const home = opts.home ?? homedir();
229
+ const override = env[SKILLS_SOURCE_ENV];
230
+ if (override !== undefined && override !== '')
231
+ return pluginSourceAt(override);
232
+ const cache = livePluginCacheDir(claudeConfigDir(env, home));
233
+ if (!existsSync(cache))
234
+ return null;
235
+ const versions = readdirSync(cache)
236
+ .filter((entry) => pluginVersionAt(join(cache, entry)) !== null)
237
+ .sort(compareVersions);
238
+ const top = versions[versions.length - 1];
239
+ if (top === undefined)
240
+ return null;
241
+ const dir = join(cache, top);
242
+ return { path: dir, kind: 'claude-plugin-cache', plugin_version: pluginVersionAt(dir) ?? top };
243
+ }
244
+ const GIT_TIMEOUT_MS = 5_000;
245
+ /** The checkout's HEAD sha + dirty flag; `{null,false}` for a non-checkout source. */
246
+ export function gitStateOf(source) {
247
+ if (source.kind !== 'checkout')
248
+ return { git_sha: null, git_dirty: false };
249
+ try {
250
+ const sha = execFileSync('git', ['rev-parse', 'HEAD'], {
251
+ cwd: source.path,
252
+ encoding: 'utf8',
253
+ timeout: GIT_TIMEOUT_MS,
254
+ stdio: ['ignore', 'pipe', 'ignore'],
255
+ }).trim();
256
+ const status = execFileSync('git', ['status', '--porcelain'], {
257
+ cwd: source.path,
258
+ encoding: 'utf8',
259
+ timeout: GIT_TIMEOUT_MS,
260
+ stdio: ['ignore', 'pipe', 'ignore'],
261
+ });
262
+ return { git_sha: sha, git_dirty: status.trim() !== '' };
263
+ }
264
+ catch (err) {
265
+ return {
266
+ git_sha: null,
267
+ git_dirty: false,
268
+ error: err instanceof Error ? err.message : String(err),
269
+ };
270
+ }
271
+ }
272
+ //# sourceMappingURL=plugin-source.js.map