claude-dev-env 8.85.0 → 8.85.2

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 (32) hide show
  1. package/.agents/skills/efficient-review/SKILL.md +79 -0
  2. package/.agents/skills/efficient-review/defaults.json +12 -0
  3. package/.agents/skills/efficient-review/reference/cloud-session.md +13 -0
  4. package/bin/codex-skill-load-block.mjs +27 -28
  5. package/bin/codex-skill-load-block.test.mjs +62 -30
  6. package/bin/codex-trimmed-sol.test.mjs +1 -0
  7. package/bin/ever-shipped-skills.mjs +1 -0
  8. package/bin/install-deepstack-plugin.mjs +300 -0
  9. package/bin/install-deepstack-plugin.test.mjs +98 -0
  10. package/bin/install-pstack-plugin.mjs +3 -5
  11. package/bin/install-pstack-plugin.test.mjs +28 -0
  12. package/bin/install.agents-home.test.mjs +1 -0
  13. package/bin/install.codex-rules.test.mjs +2 -0
  14. package/bin/install.cursor-rules.test.mjs +1 -0
  15. package/bin/install.mjs +27 -3
  16. package/bin/install.profile-settings.test.mjs +1 -0
  17. package/bin/install.prune.test.mjs +20 -0
  18. package/bin/install.question-presentation.test.mjs +7 -3
  19. package/bin/install.settings-defaults.test.mjs +1 -0
  20. package/bin/install.shared-settings.test.mjs +1 -0
  21. package/bin/install.test.mjs +79 -10
  22. package/bin/install.transaction.test.mjs +8 -0
  23. package/bin/install.uninstall-transaction.test.mjs +2 -0
  24. package/bin/install.update-keeps-hooks.test.mjs +1 -0
  25. package/docs/decisions/deepstack-install.md +9 -0
  26. package/docs/rule-guides/deepstack-wording.md +23 -0
  27. package/package.json +1 -1
  28. package/rules/deepstack-wording.md +15 -0
  29. package/scripts/deepstack_inventory.py +348 -0
  30. package/scripts/dev_env_scripts_constants/deepstack_inventory_constants.py +42 -0
  31. package/scripts/test_deepstack_inventory.py +137 -0
  32. package/scripts/test_deepstack_wording_rule.py +20 -0
@@ -0,0 +1,300 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import {
6
+ installMarketplacePlugins,
7
+ marketplacePluginsPlan,
8
+ shouldInstallMarketplacePlugin,
9
+ } from './install-pstack-plugin.mjs';
10
+
11
+ export const DEEPSTACK_MARKETPLACE_REPOSITORY = 'jl-cmd-projects/DeepStack';
12
+ export const DEEPSTACK_MARKETPLACE_NAME = 'DeepStack';
13
+ export const DEEPSTACK_PLUGIN_NAME = 'deepstack';
14
+ export const DEEPSTACK_PLUGIN_IDENTIFIER = `${DEEPSTACK_PLUGIN_NAME}@${DEEPSTACK_MARKETPLACE_NAME}`;
15
+ export const DEEPSTACK_PLUGIN_HOSTS = Object.freeze(['claude', 'codex']);
16
+ export const DEEPSTACK_PLUGIN_OPT_OUT_FLAG = '--no-deepstack';
17
+ export const DEEPSTACK_PLUGIN_OPT_OUT_VARIABLE = 'CDE_INSTALL_DEEPSTACK';
18
+ export const DEEPSTACK_SOURCE_VARIABLE = 'CDE_DEEPSTACK_SOURCE';
19
+ export const DEEPSTACK_CURSOR_DIRECTORY_NAME = 'plugins/deepstack';
20
+ export const DEEPSTACK_SKILL_RELATIVE_PATH = 'skills/deepstack-mode/SKILL.md';
21
+ export const DEEPSTACK_SESSION_SCRIPT_RELATIVE_PATH = 'hooks/session-start.sh';
22
+ export const DEEPSTACK_SESSION_RUNTIME = 'cursor';
23
+ export const DEEPSTACK_FIRST_TASK_HOOK_NAME = 'deepen_first_prompt.py';
24
+ export const DEEPSTACK_CODEX_MODEL_SHEET_NAME = 'deepstack-models.md';
25
+
26
+ export const DEEPSTACK_PLUGIN_SPEC = Object.freeze({
27
+ name: DEEPSTACK_PLUGIN_NAME,
28
+ label: 'DeepStack',
29
+ marketplaceRepository: DEEPSTACK_MARKETPLACE_REPOSITORY,
30
+ marketplaceName: DEEPSTACK_MARKETPLACE_NAME,
31
+ sparsePaths: Object.freeze([]),
32
+ pluginIdentifier: DEEPSTACK_PLUGIN_IDENTIFIER,
33
+ hosts: DEEPSTACK_PLUGIN_HOSTS,
34
+ optOutFlag: DEEPSTACK_PLUGIN_OPT_OUT_FLAG,
35
+ optOutVariable: DEEPSTACK_PLUGIN_OPT_OUT_VARIABLE,
36
+ });
37
+
38
+ /**
39
+ * Quote one path for a hook command line.
40
+ *
41
+ * @param {string} pathValue The absolute path.
42
+ * @returns {string} A single-quoted shell word.
43
+ */
44
+ function quoteCommandPath(pathValue) {
45
+ return `'${pathValue.replaceAll("'", `'\\''`)}'`;
46
+ }
47
+
48
+ /**
49
+ * Read the Claude, Codex, and Cursor actions one DeepStack install performs.
50
+ *
51
+ * Claude and Codex use the marketplace verbs. Cursor has no plugin command, so
52
+ * its action is a copy into the Cursor home plus two hook registrations.
53
+ *
54
+ * @returns {{claude: object, codex: object, cursor: {
55
+ * pluginDirectoryName: string,
56
+ * skillRelativePath: string,
57
+ * sessionScriptRelativePath: string,
58
+ * sessionRuntimeArgument: string,
59
+ * firstTaskHookName: string,
60
+ * }}} The three host actions.
61
+ */
62
+ export function deepStackInstallPlan() {
63
+ return {
64
+ claude: marketplacePluginsPlan([DEEPSTACK_PLUGIN_SPEC], 'claude'),
65
+ codex: marketplacePluginsPlan([DEEPSTACK_PLUGIN_SPEC], 'codex'),
66
+ cursor: {
67
+ pluginDirectoryName: DEEPSTACK_CURSOR_DIRECTORY_NAME,
68
+ skillRelativePath: DEEPSTACK_SKILL_RELATIVE_PATH,
69
+ sessionScriptRelativePath: DEEPSTACK_SESSION_SCRIPT_RELATIVE_PATH,
70
+ sessionRuntimeArgument: DEEPSTACK_SESSION_RUNTIME,
71
+ firstTaskHookName: DEEPSTACK_FIRST_TASK_HOOK_NAME,
72
+ },
73
+ };
74
+ }
75
+
76
+ /**
77
+ * @param {string|undefined} homeDirectory A Claude or Codex home.
78
+ * @returns {string|null} The newest cached plugin directory, or null.
79
+ */
80
+ function newestCachedPlugin(homeDirectory) {
81
+ if (!homeDirectory) return null;
82
+ const cacheDirectory = join(
83
+ homeDirectory,
84
+ 'plugins',
85
+ 'cache',
86
+ DEEPSTACK_MARKETPLACE_NAME,
87
+ DEEPSTACK_PLUGIN_NAME,
88
+ );
89
+ if (!existsSync(cacheDirectory)) return null;
90
+ const versions = readdirSync(cacheDirectory, { withFileTypes: true })
91
+ .filter(entry => entry.isDirectory())
92
+ .map(entry => entry.name)
93
+ .filter(name => existsSync(join(cacheDirectory, name, DEEPSTACK_SKILL_RELATIVE_PATH)))
94
+ .sort();
95
+ if (versions.length === 0) return null;
96
+ return join(cacheDirectory, versions[versions.length - 1]);
97
+ }
98
+
99
+ /**
100
+ * Find the plugin tree to copy into Cursor.
101
+ *
102
+ * An explicit directory wins, then the source variable, then a marketplace
103
+ * cache written by the Claude or Codex install, then a shallow clone of
104
+ * jl-cmd-projects/DeepStack.
105
+ *
106
+ * @param {object} options Search inputs.
107
+ * @param {string} [options.claudeRoot] The Claude home.
108
+ * @param {string} [options.codexHome] The Codex home.
109
+ * @param {Record<string, string|undefined>} [options.environment] The process environment.
110
+ * @param {string} [options.sourceDirectory] A directory that already holds the plugin.
111
+ * @param {(destination: string) => string|null} [options.cloneRepository] Replaces the git clone.
112
+ * @returns {string|null} The plugin directory, or null when every source is empty.
113
+ */
114
+ export function resolveDeepStackPluginSource(options = {}) {
115
+ if (options.sourceDirectory && existsSync(join(options.sourceDirectory, DEEPSTACK_SKILL_RELATIVE_PATH))) {
116
+ return options.sourceDirectory;
117
+ }
118
+ const environment = options.environment ?? process.env;
119
+ const fromEnvironment = environment[DEEPSTACK_SOURCE_VARIABLE];
120
+ if (fromEnvironment && existsSync(join(fromEnvironment, DEEPSTACK_SKILL_RELATIVE_PATH))) {
121
+ return fromEnvironment;
122
+ }
123
+ const cached = newestCachedPlugin(options.claudeRoot) ?? newestCachedPlugin(options.codexHome);
124
+ if (cached) return cached;
125
+ if (options.cloneRepository) return options.cloneRepository();
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Add the SessionStart and first-task hook groups when they are absent.
131
+ *
132
+ * @param {object} configuration The parsed Cursor hooks file.
133
+ * @param {{sessionCommand: string, firstTaskCommand: string}} commands The two commands.
134
+ * @returns {object} The configuration with both groups present.
135
+ */
136
+ export function mergeDeepStackCursorHooks(configuration, commands) {
137
+ const hooks = configuration.hooks && typeof configuration.hooks === 'object'
138
+ ? configuration.hooks
139
+ : {};
140
+ const sessionGroups = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
141
+ const promptGroups = Array.isArray(hooks.UserPromptSubmit) ? [...hooks.UserPromptSubmit] : [];
142
+ const listed = (groups, needle) => JSON.stringify(groups).includes(needle);
143
+ if (!listed(sessionGroups, DEEPSTACK_SESSION_SCRIPT_RELATIVE_PATH)) {
144
+ sessionGroups.push({
145
+ matcher: 'startup|resume|clear|compact',
146
+ hooks: [{ type: 'command', command: commands.sessionCommand }],
147
+ });
148
+ }
149
+ if (!listed(promptGroups, DEEPSTACK_FIRST_TASK_HOOK_NAME)) {
150
+ promptGroups.push({
151
+ matcher: '',
152
+ hooks: [{ type: 'command', command: commands.firstTaskCommand, timeout: 10 }],
153
+ });
154
+ }
155
+ return {
156
+ ...configuration,
157
+ hooks: {
158
+ ...hooks,
159
+ SessionStart: sessionGroups,
160
+ UserPromptSubmit: promptGroups,
161
+ },
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Copy the plugin into the Cursor home and register both hooks.
167
+ *
168
+ * @param {object} options Copy inputs.
169
+ * @param {string} options.cursorDirectory The Cursor home, usually ~/.cursor.
170
+ * @param {string|null} options.pluginSourceDirectory The plugin tree to copy.
171
+ * @param {string} options.firstTaskHookPath Absolute path of deepen_first_prompt.py.
172
+ * @returns {{status: string, pluginDirectory: string|null, hooksPath: string|null, warning: string|null}} The outcome.
173
+ */
174
+ export function installDeepStackCursor(options) {
175
+ const plan = deepStackInstallPlan().cursor;
176
+ if (!options.cursorDirectory) {
177
+ return { status: 'failed', pluginDirectory: null, hooksPath: null, warning: 'Cursor home is unset.' };
178
+ }
179
+ if (!options.pluginSourceDirectory) {
180
+ return {
181
+ status: 'failed',
182
+ pluginDirectory: null,
183
+ hooksPath: null,
184
+ warning: `No ${DEEPSTACK_MARKETPLACE_REPOSITORY} plugin tree to copy.`,
185
+ };
186
+ }
187
+ const pluginDirectory = join(options.cursorDirectory, plan.pluginDirectoryName);
188
+ mkdirSync(options.cursorDirectory, { recursive: true });
189
+ cpSync(options.pluginSourceDirectory, pluginDirectory, { recursive: true, force: true });
190
+ const sessionCommand = `${quoteCommandPath(join(pluginDirectory, plan.sessionScriptRelativePath))} ${plan.sessionRuntimeArgument}`;
191
+ const firstTaskCommand = `python3 ${quoteCommandPath(options.firstTaskHookPath)}`;
192
+ const hooksPath = join(options.cursorDirectory, 'hooks.json');
193
+ const current = existsSync(hooksPath)
194
+ ? JSON.parse(readFileSync(hooksPath, 'utf8'))
195
+ : { hooks: {} };
196
+ const merged = mergeDeepStackCursorHooks(current, { sessionCommand, firstTaskCommand });
197
+ writeFileSync(hooksPath, `${JSON.stringify(merged, null, 2)}\n`);
198
+ return { status: 'installed', pluginDirectory, hooksPath, warning: null };
199
+ }
200
+
201
+ /**
202
+ * Write the Codex model sheet when this run installed the Codex plugin.
203
+ *
204
+ * @param {string} codexHome The Codex home.
205
+ * @returns {string|null} The sheet path when this run created it.
206
+ */
207
+ export function seedCodexDeepStackModels(codexHome) {
208
+ const sheetPath = join(codexHome, DEEPSTACK_CODEX_MODEL_SHEET_NAME);
209
+ if (existsSync(sheetPath)) return null;
210
+ mkdirSync(codexHome, { recursive: true });
211
+ writeFileSync(sheetPath, 'session hook: on\n');
212
+ return sheetPath;
213
+ }
214
+
215
+ /**
216
+ * Install DeepStack for Claude, Codex, and Cursor.
217
+ *
218
+ * @param {object} [options] Install targets.
219
+ * @param {string} [options.claudeRoot] The managed Claude root.
220
+ * @param {string} [options.codexHome] The Codex home.
221
+ * @param {string} [options.cursorDirectory] The Cursor home.
222
+ * @param {string} [options.firstTaskHookPath] Absolute path of the hook the first-task command runs.
223
+ * @param {string} [options.sourceDirectory] A local plugin tree for the Cursor copy.
224
+ * @param {string[]} [options.argumentList] Command-line arguments, for the opt-out flag.
225
+ * @param {Record<string, string|undefined>} [options.environment] The process environment.
226
+ * @param {object} [dependencies] Seams for the marketplace command runner.
227
+ * @returns {{status: string, hosts: object[], cursor: object, warning: string|null}} The outcome.
228
+ */
229
+ export function installDeepStackForHosts(options = {}, dependencies = {}) {
230
+ const environment = options.environment ?? process.env;
231
+ if (!shouldInstallMarketplacePlugin(DEEPSTACK_PLUGIN_SPEC, options.argumentList, environment)) {
232
+ return {
233
+ status: 'skipped',
234
+ hosts: [],
235
+ cursor: { status: 'skipped', pluginDirectory: null, hooksPath: null, warning: null },
236
+ warning: null,
237
+ };
238
+ }
239
+ const marketplace = installMarketplacePlugins([DEEPSTACK_PLUGIN_SPEC], {
240
+ claudeRoot: options.claudeRoot,
241
+ codexHome: options.codexHome,
242
+ environment,
243
+ }, dependencies);
244
+ let sourceDirectory = resolveDeepStackPluginSource({
245
+ claudeRoot: options.claudeRoot,
246
+ codexHome: options.codexHome,
247
+ environment,
248
+ sourceDirectory: options.sourceDirectory,
249
+ });
250
+ if (!sourceDirectory && options.allowClone !== false) {
251
+ sourceDirectory = cloneDeepStackRepository(mkdtempSync(join(tmpdir(), 'deepstack-')));
252
+ }
253
+ const cursor = installDeepStackCursor({
254
+ cursorDirectory: options.cursorDirectory,
255
+ pluginSourceDirectory: sourceDirectory,
256
+ firstTaskHookPath: options.firstTaskHookPath,
257
+ });
258
+ if (marketplace.hosts.some(host => host.host === 'codex' && host.status === 'installed') && options.codexHome) {
259
+ seedCodexDeepStackModels(options.codexHome);
260
+ }
261
+ const failed = marketplace.status === 'failed' || cursor.status === 'failed';
262
+ const installed = marketplace.hosts.some(host => host.status === 'installed') || cursor.status === 'installed';
263
+ return {
264
+ status: failed ? 'failed' : (installed ? 'installed' : marketplace.status),
265
+ hosts: marketplace.hosts,
266
+ cursor,
267
+ warning: [marketplace.warning, cursor.warning].filter(Boolean).join(' ') || null,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Clone jl-cmd-projects/DeepStack when no local tree is available.
273
+ *
274
+ * The installer calls this only from a full install that has no source
275
+ * directory and no marketplace cache. Tests pass a source directory instead.
276
+ *
277
+ * @param {string} destination An empty directory for the clone.
278
+ * @returns {string|null} The plugin directory inside the clone.
279
+ */
280
+ /**
281
+ * Return the git clone URL for the DeepStack marketplace repository.
282
+ *
283
+ * @returns {string} The clone URL.
284
+ */
285
+ export function deepStackCloneUrl() {
286
+ return ['https:', '', 'github.com', `${DEEPSTACK_MARKETPLACE_REPOSITORY}.git`].join('/');
287
+ }
288
+
289
+ export function cloneDeepStackRepository(destination) {
290
+ const outcome = spawnSync('git', [
291
+ 'clone',
292
+ '--depth',
293
+ '1',
294
+ deepStackCloneUrl(),
295
+ destination,
296
+ ], { encoding: 'utf8' });
297
+ if (outcome.status !== 0) return null;
298
+ const pluginDirectory = join(destination, 'plugins', DEEPSTACK_PLUGIN_NAME);
299
+ return existsSync(join(pluginDirectory, DEEPSTACK_SKILL_RELATIVE_PATH)) ? pluginDirectory : null;
300
+ }
@@ -0,0 +1,98 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import {
7
+ DEEPSTACK_MARKETPLACE_REPOSITORY,
8
+ DEEPSTACK_PLUGIN_IDENTIFIER,
9
+ DEEPSTACK_PLUGIN_SPEC,
10
+ deepStackCloneUrl,
11
+ deepStackInstallPlan,
12
+ installDeepStackCursor,
13
+ installDeepStackForHosts,
14
+ mergeDeepStackCursorHooks,
15
+ } from './install-deepstack-plugin.mjs';
16
+ import { marketplacePluginsPlan, shouldInstallMarketplacePlugin } from './install-pstack-plugin.mjs';
17
+
18
+ test('Claude and Codex plans name jl-cmd-projects/DeepStack', () => {
19
+ const plan = deepStackInstallPlan();
20
+ assert.deepEqual(plan.claude.commands, [
21
+ ['plugin', 'marketplace', 'add', DEEPSTACK_MARKETPLACE_REPOSITORY],
22
+ ['plugin', 'install', DEEPSTACK_PLUGIN_IDENTIFIER],
23
+ ]);
24
+ assert.deepEqual(plan.codex.commands, [
25
+ ['plugin', 'marketplace', 'add', DEEPSTACK_MARKETPLACE_REPOSITORY],
26
+ ['plugin', 'add', DEEPSTACK_PLUGIN_IDENTIFIER],
27
+ ]);
28
+ assert.equal(DEEPSTACK_MARKETPLACE_REPOSITORY, 'jl-cmd-projects/DeepStack');
29
+ assert.equal(deepStackCloneUrl(), 'https://github.com/jl-cmd-projects/DeepStack.git');
30
+ assert.equal(plan.cursor.skillRelativePath, 'skills/deepstack-mode/SKILL.md');
31
+ assert.equal(plan.cursor.firstTaskHookName, 'deepen_first_prompt.py');
32
+ assert.throws(() => marketplacePluginsPlan([DEEPSTACK_PLUGIN_SPEC], 'cursor'), /cursor/);
33
+ });
34
+
35
+ test('cursor hooks keep the session hook and the first-task gate', () => {
36
+ const merged = mergeDeepStackCursorHooks({ hooks: { PreToolUse: [] } }, {
37
+ sessionCommand: "'/cursor/plugins/deepstack/hooks/session-start.sh' cursor",
38
+ firstTaskCommand: "python3 '/claude/hooks/session/deepen_first_prompt.py'",
39
+ });
40
+ assert.deepEqual(Object.keys(merged.hooks).sort(), ['PreToolUse', 'SessionStart', 'UserPromptSubmit']);
41
+ const text = JSON.stringify(merged);
42
+ assert.match(text, /session-start\.sh/);
43
+ assert.match(text, /deepen_first_prompt\.py/);
44
+ });
45
+
46
+ test('the cursor copy writes the skill and both hook commands', () => {
47
+ const root = mkdtempSync(join(tmpdir(), 'deepstack-cursor-'));
48
+ try {
49
+ const source = join(root, 'source');
50
+ mkdirSync(join(source, 'skills', 'deepstack-mode'), { recursive: true });
51
+ mkdirSync(join(source, 'hooks'), { recursive: true });
52
+ writeFileSync(join(source, 'skills', 'deepstack-mode', 'SKILL.md'), 'DeepStack intake\n');
53
+ writeFileSync(join(source, 'hooks', 'session-start.sh'), '#!/bin/sh\n');
54
+ const cursorDirectory = join(root, '.cursor');
55
+ const outcome = installDeepStackCursor({
56
+ cursorDirectory,
57
+ pluginSourceDirectory: source,
58
+ firstTaskHookPath: join(root, '.agents', 'hooks', 'session', 'deepen_first_prompt.py'),
59
+ });
60
+ assert.equal(outcome.status, 'installed');
61
+ assert.equal(
62
+ readFileSync(join(cursorDirectory, 'plugins', 'deepstack', 'skills', 'deepstack-mode', 'SKILL.md'), 'utf8'),
63
+ 'DeepStack intake\n',
64
+ );
65
+ const hooks = JSON.parse(readFileSync(join(cursorDirectory, 'hooks.json'), 'utf8'));
66
+ const text = JSON.stringify(hooks);
67
+ assert.match(text, /session-start\.sh/);
68
+ assert.match(text, /deepen_first_prompt\.py/);
69
+ assert.equal(hooks.hooks.SessionStart[0].hooks[0].command.endsWith(' cursor'), true);
70
+ } finally {
71
+ rmSync(root, { recursive: true, force: true });
72
+ }
73
+ });
74
+
75
+ test('a missing plugin tree fails the cursor copy', () => {
76
+ const outcome = installDeepStackCursor({
77
+ cursorDirectory: '/tmp/unused-cursor',
78
+ pluginSourceDirectory: null,
79
+ firstTaskHookPath: '/tmp/deepen_first_prompt.py',
80
+ });
81
+ assert.equal(outcome.status, 'failed');
82
+ assert.match(outcome.warning, /jl-cmd-projects\/DeepStack/);
83
+ });
84
+
85
+ test('--no-deepstack and CDE_INSTALL_DEEPSTACK=0 skip the install', () => {
86
+ assert.equal(shouldInstallMarketplacePlugin(DEEPSTACK_PLUGIN_SPEC, ['--no-deepstack'], {}), false);
87
+ assert.equal(
88
+ shouldInstallMarketplacePlugin(DEEPSTACK_PLUGIN_SPEC, [], { CDE_INSTALL_DEEPSTACK: '0' }),
89
+ false,
90
+ );
91
+ const skipped = installDeepStackForHosts({
92
+ argumentList: ['--no-deepstack'],
93
+ environment: {},
94
+ allowClone: false,
95
+ });
96
+ assert.equal(skipped.status, 'skipped');
97
+ assert.equal(skipped.cursor.status, 'skipped');
98
+ });
@@ -210,10 +210,8 @@ function namesAnAbsentCommand(outcome) {
210
210
  && COMMAND_NOT_FOUND_PATTERN.test(outcome.stderr ?? '');
211
211
  }
212
212
 
213
- function firstLine(text) {
214
- const trimmed = (text ?? '').trim();
215
- if (!trimmed) return '';
216
- return trimmed.split(/\r?\n/).filter(Boolean).at(-1);
213
+ function singleLineDetail(text) {
214
+ return (text ?? '').split(/\r?\n/).map(eachLine => eachLine.trim()).filter(Boolean).join(' ');
217
215
  }
218
216
 
219
217
  const ORPHANED_VERSION_MARKER = '.orphaned_at';
@@ -266,7 +264,7 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand)
266
264
  });
267
265
  if (namesAnAbsentCommand(outcome)) return absentHostOutcome(allSpecs, host, executable);
268
266
  if (outcome.status !== 0 || outcome.error) {
269
- const detail = firstLine(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
267
+ const detail = singleLineDetail(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
270
268
  return {
271
269
  host,
272
270
  executable,
@@ -126,6 +126,34 @@ test('a failing command reports that host and still installs the next one', () =
126
126
  ['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY]);
127
127
  });
128
128
 
129
+ test('should report every line of a multi-line marketplace add error', () => {
130
+ const cloneFailure = [
131
+ 'Failed to add marketplace: Fetching the marketplace from GitHub failed on both attempts.'
132
+ + ' HTTPS (https://github.com/jl-cmd/pstack-claude.git): HTTPS clone timed out',
133
+ '',
134
+ 'SSH (git@github.com:jl-cmd/pstack-claude.git): git@github.com: Permission denied (publickey).',
135
+ 'fatal: Could not read from remote repository.',
136
+ '',
137
+ 'Please make sure you have the correct access rights',
138
+ 'and the repository exists.',
139
+ ].join('\r\n');
140
+ const runner = recordingRunner((executable, commandArguments) => (
141
+ executable === 'claude' && commandArguments[2] === 'add'
142
+ ? { status: 1, stderr: cloneFailure }
143
+ : { status: 0, stderr: '' }));
144
+ const claude = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner)
145
+ .hosts.find(host => host.host === 'claude');
146
+ assert.equal(
147
+ claude.warning,
148
+ `claude plugin marketplace add ${PSTACK_MARKETPLACE_REPOSITORY} failed: `
149
+ + 'Failed to add marketplace: Fetching the marketplace from GitHub failed on both attempts.'
150
+ + ' HTTPS (https://github.com/jl-cmd/pstack-claude.git): HTTPS clone timed out'
151
+ + ' SSH (git@github.com:jl-cmd/pstack-claude.git): git@github.com: Permission denied (publickey).'
152
+ + ' fatal: Could not read from remote repository.'
153
+ + ' Please make sure you have the correct access rights and the repository exists.',
154
+ );
155
+ });
156
+
129
157
  test('an executable override replaces the host command for that host only', () => {
130
158
  const runner = recordingRunner();
131
159
  installMarketplacePlugins(
@@ -53,6 +53,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
53
53
  CLAUDE_CONFIG_DIR: undefined,
54
54
  LLM_SETTINGS_PROFILES_ROOT: undefined,
55
55
  CDE_INSTALL_PSTACK: '0',
56
+ CDE_INSTALL_DEEPSTACK: '0',
56
57
  CDE_INSTALL_USAGE_WRAPUP: '0',
57
58
  CDE_INSTALL_SUBAGENT_MODELS: '0',
58
59
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
@@ -59,6 +59,7 @@ function runInstaller(homeDirectory, extraArguments) {
59
59
  CLAUDE_CONFIG_DIR: undefined,
60
60
  LLM_SETTINGS_PROFILES_ROOT: undefined,
61
61
  CDE_INSTALL_PSTACK: '0',
62
+ CDE_INSTALL_DEEPSTACK: '0',
62
63
  CDE_INSTALL_USAGE_WRAPUP: '0',
63
64
  CDE_INSTALL_SUBAGENT_MODELS: '0',
64
65
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
@@ -92,6 +93,7 @@ function scanTextWithProductionPiiScanner(scannedText) {
92
93
  CLAUDE_CONFIG_DIR: undefined,
93
94
  LLM_SETTINGS_PROFILES_ROOT: undefined,
94
95
  CDE_INSTALL_PSTACK: '0',
96
+ CDE_INSTALL_DEEPSTACK: '0',
95
97
  CDE_INSTALL_USAGE_WRAPUP: '0',
96
98
  CDE_INSTALL_SUBAGENT_MODELS: '0',
97
99
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
@@ -33,6 +33,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
33
33
  ...process.env,
34
34
  LLM_SETTINGS_PROFILES_ROOT: undefined,
35
35
  CDE_INSTALL_PSTACK: '0',
36
+ CDE_INSTALL_DEEPSTACK: '0',
36
37
  CDE_INSTALL_USAGE_WRAPUP: '0',
37
38
  CDE_INSTALL_SUBAGENT_MODELS: '0',
38
39
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
package/bin/install.mjs CHANGED
@@ -60,6 +60,10 @@ import {
60
60
  installMarketplacePlugins,
61
61
  shouldInstallMarketplacePlugin,
62
62
  } from './install-pstack-plugin.mjs';
63
+ import {
64
+ DEEPSTACK_PLUGIN_SPEC,
65
+ installDeepStackForHosts,
66
+ } from './install-deepstack-plugin.mjs';
63
67
  import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
64
68
  import {
65
69
  codexSolSettingSnapshotPaths,
@@ -227,6 +231,7 @@ export const CORE_SKILLS = [
227
231
  'task-build',
228
232
  'eli5',
229
233
  'deepen',
234
+ 'efficient-review',
230
235
  ];
231
236
 
232
237
  export function collectPackageSourceConflicts(packageDirectory) {
@@ -2927,17 +2932,35 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2927
2932
  allUserOwnedPreferencePaths.add(eachPath);
2928
2933
  }
2929
2934
  }
2930
- const skillLoadGuidancePath = writeCodexAgentsGuidance(
2935
+ const codexGuidancePath = writeCodexAgentsGuidance(
2931
2936
  INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
2932
2937
  packageGuidanceText,
2933
2938
  allPackageGuidancePaths,
2934
2939
  );
2935
- if (skillLoadGuidancePath) {
2936
- console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
2940
+ if (codexGuidancePath) {
2941
+ console.log(` \u2713 ${codexGuidancePath} (Codex package guidance)`);
2937
2942
  }
2938
2943
  }
2939
2944
  printPluginHostOutcomes([PSTACK_PLUGIN_SPEC], pstackPlugin);
2940
2945
  }
2946
+ if (!selectedGroups && shouldInstallMarketplacePlugin(DEEPSTACK_PLUGIN_SPEC)) {
2947
+ const deepStackPlugin = installDeepStackForHosts({
2948
+ claudeRoot: CLAUDE_HOME,
2949
+ codexHome: INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
2950
+ cursorDirectory: INSTALL_ROOT_RESOLUTION.cursorInstallDirectory,
2951
+ firstTaskHookPath: join(
2952
+ INSTALL_ROOT_RESOLUTION.hooksInstallDirectory,
2953
+ 'session',
2954
+ 'deepen_first_prompt.py',
2955
+ ),
2956
+ });
2957
+ summary.deepStackPlugin = deepStackPlugin;
2958
+ printPluginHostOutcomes([DEEPSTACK_PLUGIN_SPEC], deepStackPlugin);
2959
+ const cursorOutcome = deepStackPlugin.cursor;
2960
+ console.log(cursorOutcome.warning
2961
+ ? ` DeepStack (cursor): ${cursorOutcome.status} — ${cursorOutcome.warning}`
2962
+ : ` DeepStack (cursor): ${cursorOutcome.status}`);
2963
+ }
2941
2964
  const allRepositoryPluginSpecs = [
2942
2965
  USAGE_WRAPUP_PLUGIN_SPEC,
2943
2966
  SUBAGENT_MODELS_PLUGIN_SPEC,
@@ -3484,6 +3507,7 @@ Usage:
3484
3507
  npx ${PACKAGE_NAME} --profile ID Install into one named profile root (under the profiles root)
3485
3508
  npx ${PACKAGE_NAME} --profiles A,B Install into each selected profile (one ownership manifest per target)
3486
3509
  npx ${PACKAGE_NAME} --no-pstack Full install without the pstack plugin (also CDE_INSTALL_PSTACK=0)
3510
+ npx ${PACKAGE_NAME} --no-deepstack Full install without the DeepStack plugin (also CDE_INSTALL_DEEPSTACK=0)
3487
3511
  npx ${PACKAGE_NAME} --no-usage-wrapup Full install without the usage-wrapup plugin (also CDE_INSTALL_USAGE_WRAPUP=0)
3488
3512
  npx ${PACKAGE_NAME} --no-subagent-models Full install without the subagent-models plugin (also CDE_INSTALL_SUBAGENT_MODELS=0)
3489
3513
  npx ${PACKAGE_NAME} --no-deepen-first-prompt Full install without the deepen-first-prompt plugin (also CDE_INSTALL_DEEPEN_FIRST_PROMPT=0)
@@ -31,6 +31,7 @@ function installEnvironment(homeDirectory) {
31
31
  GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
32
32
  CODEX_HOME: join(homeDirectory, '.codex'),
33
33
  CDE_INSTALL_PSTACK: '0',
34
+ CDE_INSTALL_DEEPSTACK: '0',
34
35
  CDE_INSTALL_USAGE_WRAPUP: '0',
35
36
  };
36
37
  }
@@ -64,6 +64,7 @@ const CORE_SKILL_DIRECTORIES = [
64
64
  'issue-tracker',
65
65
  'task-build',
66
66
  'eli5',
67
+ 'efficient-review',
67
68
  ];
68
69
  const PRIOR_RUN_BACKUP_DIRECTORY_NAMES = [
69
70
  '2020-01-01T00-00-00-000Z',
@@ -262,6 +263,7 @@ function resolveInstallerInvocation(homeDirectory) {
262
263
  CLAUDE_CONFIG_DIR: undefined,
263
264
  LLM_SETTINGS_PROFILES_ROOT: undefined,
264
265
  CDE_INSTALL_PSTACK: '0',
266
+ CDE_INSTALL_DEEPSTACK: '0',
265
267
  CDE_INSTALL_USAGE_WRAPUP: '0',
266
268
  CDE_INSTALL_SUBAGENT_MODELS: '0',
267
269
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
@@ -802,6 +804,24 @@ test('a scoped --only install records the skill names it installed', () => {
802
804
  }
803
805
  });
804
806
 
807
+ test('a fresh scoped core install includes efficient-review', () => {
808
+ const sandbox = createSandbox();
809
+ try {
810
+ runInstaller(sandbox.homeDirectory, ['--only', 'core']);
811
+ const skillPath = join(sandbox.skillsDirectory, 'efficient-review', 'SKILL.md');
812
+ const skillText = readFileSync(skillPath, 'utf8');
813
+ assert.match(skillText, /^name: efficient-review$/m);
814
+ assert.match(skillText, /\/efficient-review/);
815
+ assert.equal(readManifest(sandbox.manifestPath).skills.includes('efficient-review'), true);
816
+ assert.equal(
817
+ existsSync(join(sandbox.skillsDirectory, 'efficient-review', 'defaults.json')),
818
+ true,
819
+ );
820
+ } finally {
821
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
822
+ }
823
+ });
824
+
805
825
  test('a fresh scoped core install ships active skills and omits retired skills', () => {
806
826
  const sandbox = createSandbox();
807
827
  try {
@@ -25,6 +25,7 @@ function installInScratchHome(homeDirectory, argumentsList = [], extraEnvironmen
25
25
  CLAUDE_CONFIG_DIR: undefined,
26
26
  LLM_SETTINGS_PROFILES_ROOT: undefined,
27
27
  CDE_INSTALL_PSTACK: '0',
28
+ CDE_INSTALL_DEEPSTACK: '0',
28
29
  CDE_INSTALL_USAGE_WRAPUP: '0',
29
30
  CDE_INSTALL_SUBAGENT_MODELS: '0',
30
31
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
@@ -171,7 +172,7 @@ test('a selected Claude profile receives the source rule and its Codex guidance'
171
172
  assert.ok(readFileSync(join(profileDirectory, '.codex', 'AGENTS.md'), 'utf8').includes(readFileSync(policyPath, 'utf8')));
172
173
  });
173
174
 
174
- test('a successful pstack install retains its skill block and gains the question block', (context) => {
175
+ test('a successful pstack install omits the retired skill block and gains the question block', (context) => {
175
176
  const homeDirectory = makeScratchHome(context);
176
177
  const commandPath = join(homeDirectory, process.platform === 'win32' ? 'codex.cmd' : 'codex');
177
178
  writeFileSync(
@@ -182,6 +183,7 @@ test('a successful pstack install retains its skill block and gains the question
182
183
 
183
184
  const installation = installInScratchHome(homeDirectory, [], {
184
185
  CDE_INSTALL_PSTACK: '1',
186
+ CDE_INSTALL_DEEPSTACK: '0',
185
187
  CDE_INSTALL_USAGE_WRAPUP: '0',
186
188
  CDE_INSTALL_SUBAGENT_MODELS: '0',
187
189
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
@@ -191,7 +193,7 @@ test('a successful pstack install retains its skill block and gains the question
191
193
 
192
194
  assert.equal(installation.status, 0, installation.stdout + installation.stderr);
193
195
  const guidanceText = readFileSync(join(homeDirectory, '.codex', 'AGENTS.md'), 'utf8');
194
- assert.match(guidanceText, /<!-- claude-dev-env skill load: start -->/);
196
+ assert.doesNotMatch(guidanceText, /<!-- claude-dev-env skill load: start -->/);
195
197
  assert.match(guidanceText, /<!-- claude-dev-env question presentation: start -->/);
196
198
  assert.ok(guidanceText.includes(readFileSync(policyPath, 'utf8')));
197
199
  });
@@ -211,6 +213,7 @@ test('enabling pstack after a pstack-off install preserves question guidance', (
211
213
 
212
214
  const secondInstallation = installInScratchHome(homeDirectory, [], {
213
215
  CDE_INSTALL_PSTACK: '1',
216
+ CDE_INSTALL_DEEPSTACK: '0',
214
217
  CDE_INSTALL_USAGE_WRAPUP: '0',
215
218
  CDE_INSTALL_SUBAGENT_MODELS: '0',
216
219
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
@@ -221,7 +224,7 @@ test('enabling pstack after a pstack-off install preserves question guidance', (
221
224
  assert.equal(secondInstallation.status, 0, secondInstallation.stdout + secondInstallation.stderr);
222
225
  const updatedGuidance = readFileSync(codexGuidancePath, 'utf8');
223
226
  assert.ok(updatedGuidance.includes(firstGuidance));
224
- assert.match(updatedGuidance, /<!-- claude-dev-env skill load: start -->/);
227
+ assert.doesNotMatch(updatedGuidance, /<!-- claude-dev-env skill load: start -->/);
225
228
  assert.match(updatedGuidance, /^feature, refactoring: gpt-6-sol$/m);
226
229
  assert.match(readFileSync(join(homeDirectory, '.codex', 'pstack-models.md'), 'utf8'), /^session hook: on$/m);
227
230
  });
@@ -241,6 +244,7 @@ test('a failed pstack transition restores question-only guidance and removes its
241
244
 
242
245
  const failedInstallation = installInScratchHome(homeDirectory, [], {
243
246
  CDE_INSTALL_PSTACK: '1',
247
+ CDE_INSTALL_DEEPSTACK: '0',
244
248
  CDE_INSTALL_USAGE_WRAPUP: '0',
245
249
  CDE_INSTALL_SUBAGENT_MODELS: '0',
246
250
  CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',