@the-open-engine/zeroshot 6.3.0 → 6.5.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 (149) hide show
  1. package/cli/commands/providers.js +11 -5
  2. package/cli/index.js +83 -17
  3. package/cli/lib/first-run.js +14 -3
  4. package/docker/zeroshot-cluster/Dockerfile +11 -1
  5. package/lib/agent-cli-provider/acp-stdio-runner.d.ts +4 -0
  6. package/lib/agent-cli-provider/acp-stdio-runner.d.ts.map +1 -0
  7. package/lib/agent-cli-provider/acp-stdio-runner.js +259 -0
  8. package/lib/agent-cli-provider/acp-stdio-runner.js.map +1 -0
  9. package/lib/agent-cli-provider/adapters/acp.d.ts +23 -0
  10. package/lib/agent-cli-provider/adapters/acp.d.ts.map +1 -0
  11. package/lib/agent-cli-provider/adapters/acp.js +373 -0
  12. package/lib/agent-cli-provider/adapters/acp.js.map +1 -0
  13. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  14. package/lib/agent-cli-provider/adapters/claude.js +2 -29
  15. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  16. package/lib/agent-cli-provider/adapters/copilot-parser.d.ts +3 -0
  17. package/lib/agent-cli-provider/adapters/copilot-parser.d.ts.map +1 -0
  18. package/lib/agent-cli-provider/adapters/copilot-parser.js +154 -0
  19. package/lib/agent-cli-provider/adapters/copilot-parser.js.map +1 -0
  20. package/lib/agent-cli-provider/adapters/copilot.d.ts +3 -0
  21. package/lib/agent-cli-provider/adapters/copilot.d.ts.map +1 -0
  22. package/lib/agent-cli-provider/adapters/copilot.js +176 -0
  23. package/lib/agent-cli-provider/adapters/copilot.js.map +1 -0
  24. package/lib/agent-cli-provider/adapters/gateway.d.ts +6 -0
  25. package/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -0
  26. package/lib/agent-cli-provider/adapters/gateway.js +161 -0
  27. package/lib/agent-cli-provider/adapters/gateway.js.map +1 -0
  28. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
  29. package/lib/agent-cli-provider/adapters/gemini.js +9 -2
  30. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
  31. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  32. package/lib/agent-cli-provider/adapters/index.js +6 -34
  33. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  34. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  35. package/lib/agent-cli-provider/adapters/opencode.js +5 -1
  36. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  37. package/lib/agent-cli-provider/adapters/pi.d.ts +3 -0
  38. package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -0
  39. package/lib/agent-cli-provider/adapters/pi.js +331 -0
  40. package/lib/agent-cli-provider/adapters/pi.js.map +1 -0
  41. package/lib/agent-cli-provider/claude-command.d.ts +6 -0
  42. package/lib/agent-cli-provider/claude-command.d.ts.map +1 -0
  43. package/lib/agent-cli-provider/claude-command.js +43 -0
  44. package/lib/agent-cli-provider/claude-command.js.map +1 -0
  45. package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
  46. package/lib/agent-cli-provider/contract-actions.js +13 -17
  47. package/lib/agent-cli-provider/contract-actions.js.map +1 -1
  48. package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
  49. package/lib/agent-cli-provider/contract-invoke.js +10 -3
  50. package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
  51. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  52. package/lib/agent-cli-provider/contract-options.js +57 -2
  53. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  54. package/lib/agent-cli-provider/contract-support.d.ts +1 -0
  55. package/lib/agent-cli-provider/contract-support.d.ts.map +1 -1
  56. package/lib/agent-cli-provider/contract-support.js +25 -3
  57. package/lib/agent-cli-provider/contract-support.js.map +1 -1
  58. package/lib/agent-cli-provider/errors.d.ts +1 -0
  59. package/lib/agent-cli-provider/errors.d.ts.map +1 -1
  60. package/lib/agent-cli-provider/errors.js +14 -5
  61. package/lib/agent-cli-provider/errors.js.map +1 -1
  62. package/lib/agent-cli-provider/gateway-client.d.ts +32 -0
  63. package/lib/agent-cli-provider/gateway-client.d.ts.map +1 -0
  64. package/lib/agent-cli-provider/gateway-client.js +135 -0
  65. package/lib/agent-cli-provider/gateway-client.js.map +1 -0
  66. package/lib/agent-cli-provider/gateway-runner.d.ts +9 -0
  67. package/lib/agent-cli-provider/gateway-runner.d.ts.map +1 -0
  68. package/lib/agent-cli-provider/gateway-runner.js +294 -0
  69. package/lib/agent-cli-provider/gateway-runner.js.map +1 -0
  70. package/lib/agent-cli-provider/gateway-tools.d.ts +14 -0
  71. package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -0
  72. package/lib/agent-cli-provider/gateway-tools.js +495 -0
  73. package/lib/agent-cli-provider/gateway-tools.js.map +1 -0
  74. package/lib/agent-cli-provider/index.d.ts +3 -2
  75. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  76. package/lib/agent-cli-provider/index.js +14 -1
  77. package/lib/agent-cli-provider/index.js.map +1 -1
  78. package/lib/agent-cli-provider/invoke-evidence.d.ts +2 -2
  79. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
  80. package/lib/agent-cli-provider/invoke-evidence.js +17 -3
  81. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
  82. package/lib/agent-cli-provider/provider-registry.d.ts +437 -0
  83. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -0
  84. package/lib/agent-cli-provider/provider-registry.js +411 -0
  85. package/lib/agent-cli-provider/provider-registry.js.map +1 -0
  86. package/lib/agent-cli-provider/single-agent-runtime.d.ts +7 -0
  87. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  88. package/lib/agent-cli-provider/single-agent-runtime.js +139 -19
  89. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  90. package/lib/agent-cli-provider/types.d.ts +106 -4
  91. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  92. package/lib/agent-cli-provider/types.js.map +1 -1
  93. package/lib/detached-startup.js +4 -2
  94. package/lib/docker-config.js +24 -13
  95. package/lib/provider-defaults.js +15 -4
  96. package/lib/provider-detection.js +2 -0
  97. package/lib/provider-names.js +32 -12
  98. package/lib/repo-settings.js +15 -1
  99. package/lib/run-mode.js +18 -6
  100. package/lib/run-plan.js +32 -0
  101. package/lib/settings/claude-auth.js +3 -6
  102. package/lib/settings.js +17 -2
  103. package/lib/setup-apply.js +300 -0
  104. package/lib/setup-journal.js +109 -0
  105. package/lib/setup-plan.js +406 -0
  106. package/lib/setup-undo.js +88 -0
  107. package/lib/start-cluster.js +34 -9
  108. package/lib/stream-json-parser.js +7 -5
  109. package/package.json +33 -3
  110. package/scripts/live-provider-smoke.js +221 -0
  111. package/src/agent/agent-lifecycle.js +7 -4
  112. package/src/agent/agent-quality-gate-schema.js +2 -2
  113. package/src/agent/agent-task-executor.js +41 -2
  114. package/src/agent-cli-provider/acp-stdio-runner.ts +314 -0
  115. package/src/agent-cli-provider/adapters/acp.ts +493 -0
  116. package/src/agent-cli-provider/adapters/claude.ts +2 -32
  117. package/src/agent-cli-provider/adapters/copilot-parser.ts +166 -0
  118. package/src/agent-cli-provider/adapters/copilot.ts +231 -0
  119. package/src/agent-cli-provider/adapters/gateway.ts +186 -0
  120. package/src/agent-cli-provider/adapters/gemini.ts +9 -2
  121. package/src/agent-cli-provider/adapters/index.ts +5 -36
  122. package/src/agent-cli-provider/adapters/opencode.ts +4 -1
  123. package/src/agent-cli-provider/adapters/pi.ts +410 -0
  124. package/src/agent-cli-provider/claude-command.ts +47 -0
  125. package/src/agent-cli-provider/contract-actions.ts +14 -17
  126. package/src/agent-cli-provider/contract-invoke.ts +12 -3
  127. package/src/agent-cli-provider/contract-options.ts +72 -3
  128. package/src/agent-cli-provider/contract-support.ts +30 -3
  129. package/src/agent-cli-provider/errors.ts +14 -4
  130. package/src/agent-cli-provider/gateway-client.ts +170 -0
  131. package/src/agent-cli-provider/gateway-runner.ts +353 -0
  132. package/src/agent-cli-provider/gateway-tools.ts +616 -0
  133. package/src/agent-cli-provider/index.ts +28 -0
  134. package/src/agent-cli-provider/invoke-evidence.ts +24 -3
  135. package/src/agent-cli-provider/provider-registry.ts +515 -0
  136. package/src/agent-cli-provider/single-agent-runtime.ts +192 -18
  137. package/src/agent-cli-provider/types.ts +145 -4
  138. package/src/config-validator.js +33 -15
  139. package/src/isolation-manager.js +124 -19
  140. package/src/ledger.js +3 -0
  141. package/src/message-bus.js +12 -2
  142. package/src/orchestrator.js +37 -17
  143. package/src/preflight.js +83 -30
  144. package/src/providers/capabilities.js +15 -42
  145. package/src/providers/index.js +52 -72
  146. package/src/worktree-claude-config.js +17 -0
  147. package/task-lib/commands/run.js +1 -0
  148. package/task-lib/provider-helper-runtime.js +11 -0
  149. package/task-lib/runner.js +7 -0
package/lib/settings.js CHANGED
@@ -33,6 +33,14 @@ function getSettingsFile() {
33
33
  );
34
34
  }
35
35
 
36
+ /**
37
+ * Whether the global settings file exists on disk (vs. running on defaults).
38
+ * @returns {boolean}
39
+ */
40
+ function settingsFileExists() {
41
+ return fs.existsSync(getSettingsFile());
42
+ }
43
+
36
44
  // Import provider defaults from separate module to avoid circular dependency
37
45
  const { getProviderDefaults, clearProviderDefaultsCache } = require('./provider-defaults');
38
46
 
@@ -103,6 +111,7 @@ const DEFAULT_SETTINGS_BASE = {
103
111
  },
104
112
  defaultConfig: 'conductor-bootstrap',
105
113
  defaultDocker: false,
114
+ defaultDelivery: 'none', // 'none' | 'pr' | 'ship' - folded into resolveEffectiveRunPlan same as defaultDocker
106
115
  strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
107
116
  logLevel: 'normal',
108
117
  // Auto-update settings
@@ -113,7 +122,7 @@ const DEFAULT_SETTINGS_BASE = {
113
122
  // Example: 'ccr code' for claude-code-router integration
114
123
  claudeCommand: 'claude',
115
124
  // Docker isolation mounts - preset names or {host, container, readonly?} objects
116
- // Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini, opencode
125
+ // Valid presets: infrastructure presets plus provider ids from the provider registry
117
126
  dockerMounts: ['gh', 'git', 'ssh'],
118
127
  // Extra env vars to pass to Docker container (in addition to preset-implied ones)
119
128
  // Supports: VAR (if set), VAR_* (pattern), VAR=value (forced), VAR= (empty)
@@ -323,7 +332,7 @@ function validateProviderSettings(value) {
323
332
 
324
333
  for (const [providerName, settings] of Object.entries(normalizedSettings || {})) {
325
334
  if (!VALID_PROVIDERS.includes(providerName)) {
326
- return `Unknown provider in providerSettings: ${providerName}`;
335
+ return `Unknown provider in providerSettings: ${providerName}. Valid providers: ${VALID_PROVIDERS.join(', ')}`;
327
336
  }
328
337
 
329
338
  // Delegate validation to the provider
@@ -391,6 +400,10 @@ function validateSetting(key, value) {
391
400
  return `Invalid log level: ${value}. Valid levels: quiet, normal, verbose`;
392
401
  }
393
402
 
403
+ if (key === 'defaultDelivery' && !['none', 'pr', 'ship'].includes(value)) {
404
+ return `Invalid defaultDelivery: ${value}. Valid: none, pr, ship`;
405
+ }
406
+
394
407
  if (key === 'claudeCommand') {
395
408
  return validateClaudeCommand(value);
396
409
  }
@@ -528,6 +541,7 @@ module.exports = {
528
541
  coerceValue,
529
542
  DEFAULT_SETTINGS,
530
543
  getSettingsFile,
544
+ settingsFileExists,
531
545
  getClaudeCommand,
532
546
  // Model validation exports
533
547
  MODEL_HIERARCHY,
@@ -535,6 +549,7 @@ module.exports = {
535
549
  validateModelAgainstMax,
536
550
  // Provider defaults exports
537
551
  clearProviderDefaultsCache,
552
+ mapLegacyModelToLevel,
538
553
  // Backward compatibility: SETTINGS_FILE as getter (reads env var dynamically)
539
554
  get SETTINGS_FILE() {
540
555
  return getSettingsFile();
@@ -0,0 +1,300 @@
1
+ /**
2
+ * `zeroshot setup apply` — writes decisions from #A's plan/decision contract
3
+ * (lib/setup-plan.js) to global settings, repo-local settings, and the undo
4
+ * journal (lib/setup-journal.js).
5
+ *
6
+ * Fail-closed: every input decisionId + value is resolved and validated
7
+ * against its domain before ANY write happens. Writes are then applied only
8
+ * to settings keys a run-mode resolver actually reads (setup-plan.js's
9
+ * CONSUMED_PATHS/isConsumedPath — the same set that filters proposedWrites)
10
+ * — a settings key nobody reads is dead config, the exact drift the
11
+ * canonical-path rule (issue #605) forbids.
12
+ */
13
+
14
+ const { resolveDecisionPath, domainFor, isConsumedPath, CONSUMED_PATHS } = require('./setup-plan');
15
+ const { validateMountConfig, validateEnvPassthrough } = require('./docker-config');
16
+ const { VALID_PROVIDERS } = require('./provider-names');
17
+ const { VALID_MODELS, mapLegacyModelToLevel } = require('./settings');
18
+ const {
19
+ loadJournal,
20
+ saveJournal,
21
+ upsertJournalEntry,
22
+ getNestedValue,
23
+ setNestedValue,
24
+ deepEqual,
25
+ } = require('./setup-journal');
26
+
27
+ const SECRET_PATTERN = /token|secret|password|api[_-]?key|credential/i;
28
+
29
+ function assertSecretSafePath(targetPath) {
30
+ if (SECRET_PATTERN.test(targetPath)) {
31
+ throw new Error(`Refusing to write secret-shaped settings path: ${targetPath}`);
32
+ }
33
+ }
34
+
35
+ function domainError(decisionId, value) {
36
+ return new Error(
37
+ `Invalid value for decision "${decisionId}": expected ${domainFor(decisionId)}, got ${JSON.stringify(value)}`
38
+ );
39
+ }
40
+
41
+ // Resolves a submitted decision value into the raw form actually stored at
42
+ // its target settings path, validating it against #A's domain first.
43
+ // (defaultIsolation's domain is worktree|docker|none, but its target path is
44
+ // the boolean settings.defaultDocker — this is where that translation lives.)
45
+ function convertDecisionValue({ decisionId, value, globalSettings, deps }) {
46
+ switch (decisionId) {
47
+ case 'defaultProvider':
48
+ if (typeof value !== 'string' || !deps.VALID_PROVIDERS.includes(value)) {
49
+ throw domainError(decisionId, value);
50
+ }
51
+ return value;
52
+
53
+ case 'defaultIsolation':
54
+ if (!['worktree', 'docker', 'none'].includes(value)) throw domainError(decisionId, value);
55
+ return value === 'docker';
56
+
57
+ case 'allowLocalNoIsolation':
58
+ if (typeof value !== 'boolean') throw domainError(decisionId, value);
59
+ return value;
60
+
61
+ case 'defaultDelivery':
62
+ if (!['none', 'pr', 'ship'].includes(value)) throw domainError(decisionId, value);
63
+ return value;
64
+
65
+ case 'defaultIssueSource':
66
+ if (typeof value !== 'string' || !deps.listIssueProviders().includes(value)) {
67
+ throw domainError(decisionId, value);
68
+ }
69
+ return value;
70
+
71
+ case 'prBase':
72
+ if (typeof value !== 'string' || value.trim() === '') throw domainError(decisionId, value);
73
+ return value;
74
+
75
+ case 'dockerMounts': {
76
+ const err = validateMountConfig(value);
77
+ if (err) throw new Error(`Invalid value for decision "${decisionId}": ${err}`);
78
+ return value;
79
+ }
80
+
81
+ case 'dockerEnvPassthrough': {
82
+ const err = validateEnvPassthrough(value);
83
+ if (err) throw new Error(`Invalid value for decision "${decisionId}": ${err}`);
84
+ return value;
85
+ }
86
+
87
+ case 'updatePolicy':
88
+ if (!['off', 'notify', 'auto'].includes(value)) throw domainError(decisionId, value);
89
+ return value;
90
+
91
+ default:
92
+ if (decisionId.startsWith('providerLevel.')) {
93
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
94
+ throw domainError(decisionId, value);
95
+ }
96
+ for (const key of ['min', 'default', 'max']) {
97
+ if (!VALID_MODELS.includes(value[key])) throw domainError(decisionId, value);
98
+ }
99
+ const providerName = decisionId.slice('providerLevel.'.length);
100
+ const existing = getNestedValue(globalSettings, `providerSettings.${providerName}`) || {};
101
+ return {
102
+ ...existing,
103
+ minLevel: mapLegacyModelToLevel(value.min),
104
+ defaultLevel: mapLegacyModelToLevel(value.default),
105
+ maxLevel: mapLegacyModelToLevel(value.max),
106
+ };
107
+ }
108
+ throw new Error(`Unknown decision ID: ${decisionId}`);
109
+ }
110
+ }
111
+
112
+ function defaultApplyDeps() {
113
+ const fs = require('fs');
114
+ const { loadSettings, saveSettings } = require('./settings');
115
+ const { readRepoSettings, writeRepoSettings } = require('./repo-settings');
116
+ const { checkGhAuth } = require('../src/preflight');
117
+ const { listProviders: listIssueProviders } = require('../src/issue-providers');
118
+
119
+ return {
120
+ readFile: (filePath) => fs.readFileSync(filePath, 'utf8'),
121
+ loadSettings,
122
+ saveSettings,
123
+ readRepoSettings,
124
+ writeRepoSettings,
125
+ checkGhAuth,
126
+ listIssueProviders,
127
+ VALID_PROVIDERS,
128
+ now: () => new Date().toISOString(),
129
+ };
130
+ }
131
+
132
+ // Phase 1: resolve + validate EVERY input decision before any write happens
133
+ // (fail-closed — a single bad decisionId or out-of-domain value rejects the
134
+ // whole request, never a partial apply).
135
+ function resolveAndValidateDecisions(input, globalSettings, deps) {
136
+ const resolved = [];
137
+ for (const [decisionId, value] of Object.entries(input)) {
138
+ const target = resolveDecisionPath(decisionId);
139
+ if (!target) {
140
+ throw new Error(`Unknown decision ID: ${decisionId}`);
141
+ }
142
+ assertSecretSafePath(target.path);
143
+ const writeValue = convertDecisionValue({ decisionId, value, globalSettings, deps });
144
+ resolved.push({ decisionId, target, inputValue: value, writeValue });
145
+ }
146
+ return resolved;
147
+ }
148
+
149
+ // Decides the outcome for a single resolved decision: a skip (with reason)
150
+ // or the write to perform. Returns the write descriptor rather than mutating
151
+ // anything, so the caller controls when settings objects/journal are touched.
152
+ function resolveDecisionOutcome(
153
+ { decisionId, target, inputValue, writeValue },
154
+ { globalSettings, repoSettings, allowRiskyDefaults }
155
+ ) {
156
+ const settingsObj = target.scope === 'repo' ? repoSettings : globalSettings;
157
+ const currentValue = getNestedValue(settingsObj, target.path) ?? null;
158
+ const base = { decisionId, from: currentValue, to: writeValue };
159
+
160
+ if (decisionId === 'defaultDelivery' && inputValue === 'ship' && !allowRiskyDefaults) {
161
+ return { result: { ...base, applied: false, skippedReason: 'requires-explicit-opt-in' } };
162
+ }
163
+ if (!isConsumedPath(target.scope, target.path)) {
164
+ return { result: { ...base, applied: false, skippedReason: 'no-consumer' } };
165
+ }
166
+ if (deepEqual(currentValue, writeValue)) {
167
+ return { result: { ...base, applied: false, skippedReason: 'unchanged' } };
168
+ }
169
+
170
+ return {
171
+ result: { ...base, applied: true },
172
+ write: {
173
+ settingsObj,
174
+ scope: target.scope,
175
+ path: target.path,
176
+ writeValue,
177
+ priorValue: currentValue,
178
+ },
179
+ };
180
+ }
181
+
182
+ // Applies one decision's write to its in-memory settings object and journals
183
+ // it (deferred persistence — the caller flushes to disk once at the end).
184
+ function applyWrite(write, { repoRoot, journal, deps }) {
185
+ setNestedValue(write.settingsObj, write.path, write.writeValue);
186
+ upsertJournalEntry(journal, {
187
+ scope: write.scope,
188
+ path: write.path,
189
+ repoRoot: write.scope === 'repo' ? repoRoot : null,
190
+ priorValue: write.priorValue,
191
+ appliedValue: write.writeValue,
192
+ appliedAt: deps.now(),
193
+ });
194
+ return write.scope;
195
+ }
196
+
197
+ function persistIfDirty({
198
+ globalDirty,
199
+ repoDirty,
200
+ globalSettings,
201
+ repoSettings,
202
+ repoRoot,
203
+ journal,
204
+ deps,
205
+ }) {
206
+ if (globalDirty) deps.saveSettings(globalSettings);
207
+ if (repoDirty) deps.writeRepoSettings(repoRoot, repoSettings);
208
+ if (globalDirty || repoDirty) saveJournal(journal);
209
+ }
210
+
211
+ // Phase 2: write every resolved decision, journaling each mutation and
212
+ // skipping (never throwing) decisions that are unchanged, risky-without-optin,
213
+ // or targeted at a settings key no resolver consumes.
214
+ function writeResolvedDecisions(
215
+ resolved,
216
+ { globalSettings, repoSettings, repoRoot, journal, allowRiskyDefaults, deps }
217
+ ) {
218
+ const results = [];
219
+ let globalDirty = false;
220
+ let repoDirty = false;
221
+
222
+ for (const decision of resolved) {
223
+ const { result, write } = resolveDecisionOutcome(decision, {
224
+ globalSettings,
225
+ repoSettings,
226
+ allowRiskyDefaults,
227
+ });
228
+ results.push(result);
229
+ if (!write) continue;
230
+
231
+ const scope = applyWrite(write, { repoRoot, journal, deps });
232
+ if (scope === 'repo') repoDirty = true;
233
+ else globalDirty = true;
234
+ }
235
+
236
+ persistIfDirty({ globalDirty, repoDirty, globalSettings, repoSettings, repoRoot, journal, deps });
237
+
238
+ return results;
239
+ }
240
+
241
+ /**
242
+ * Apply a decisions file `{ "<decisionId>": <value>, ... }` to settings.
243
+ *
244
+ * @param {Object} params
245
+ * @param {string} params.decisionsPath - Path to the decisions JSON file.
246
+ * @param {string} params.cwd - Working directory (for repo-scope settings lookup).
247
+ * @param {boolean} [params.allowRiskyDefaults] - Required to store defaultDelivery='ship'.
248
+ * @param {Object} [params.deps] - Injected dependencies (for testing).
249
+ * @returns {Array<{decisionId: string, applied: boolean, from: *, to: *, skippedReason?: string}>}
250
+ */
251
+ function applyDecisions({ decisionsPath, cwd, allowRiskyDefaults = false, deps = {} }) {
252
+ const resolvedDeps = { ...defaultApplyDeps(), ...deps };
253
+
254
+ let input;
255
+ try {
256
+ input = JSON.parse(resolvedDeps.readFile(decisionsPath));
257
+ } catch (err) {
258
+ throw new Error(`Failed to read decisions file "${decisionsPath}": ${err.message}`);
259
+ }
260
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
261
+ throw new Error('Decisions file must be a JSON object of { decisionId: value }');
262
+ }
263
+
264
+ const globalSettings = resolvedDeps.loadSettings();
265
+ const { repoRoot, settings: repoSettingsRaw } = resolvedDeps.readRepoSettings(cwd);
266
+ const repoSettings = repoSettingsRaw || {};
267
+
268
+ const resolved = resolveAndValidateDecisions(input, globalSettings, resolvedDeps);
269
+
270
+ const results = writeResolvedDecisions(resolved, {
271
+ globalSettings,
272
+ repoSettings,
273
+ repoRoot,
274
+ journal: loadJournal(),
275
+ allowRiskyDefaults,
276
+ deps: resolvedDeps,
277
+ });
278
+
279
+ // Never store provider-auth secrets: print the login command instead.
280
+ const issueSourceApplied = results.find(
281
+ (r) => r.decisionId === 'defaultIssueSource' && r.applied
282
+ );
283
+ if (issueSourceApplied && issueSourceApplied.to === 'github') {
284
+ const auth = resolvedDeps.checkGhAuth();
285
+ if (!auth || !auth.authenticated) {
286
+ console.log('Run: gh auth login');
287
+ }
288
+ }
289
+
290
+ return results;
291
+ }
292
+
293
+ module.exports = {
294
+ applyDecisions,
295
+ resolveAndValidateDecisions,
296
+ writeResolvedDecisions,
297
+ assertSecretSafePath,
298
+ isConsumedPath,
299
+ CONSUMED_PATHS,
300
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Undo journal for `zeroshot setup apply` / `zeroshot setup undo`.
3
+ *
4
+ * One journal entry per setting this setup owns: { scope, path, repoRoot,
5
+ * priorValue, appliedValue, appliedAt }. `priorValue: null` means the key
6
+ * did not exist before apply, so undo deletes it rather than restoring it.
7
+ * Shared by lib/setup-apply.js (writer) and lib/setup-undo.js (reader) so the
8
+ * nested-path mutation and equality semantics can't drift between the two.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { getSettingsFile } = require('./settings');
14
+ const { getNestedValue } = require('./setup-plan');
15
+
16
+ function getJournalPath() {
17
+ return path.join(path.dirname(getSettingsFile()), 'setup-undo-journal.json');
18
+ }
19
+
20
+ function loadJournal() {
21
+ const journalPath = getJournalPath();
22
+ if (!fs.existsSync(journalPath)) {
23
+ return { version: 1, entries: [] };
24
+ }
25
+ try {
26
+ const parsed = JSON.parse(fs.readFileSync(journalPath, 'utf8'));
27
+ if (!parsed || !Array.isArray(parsed.entries)) {
28
+ return { version: 1, entries: [] };
29
+ }
30
+ return parsed;
31
+ } catch {
32
+ return { version: 1, entries: [] };
33
+ }
34
+ }
35
+
36
+ function saveJournal(journal) {
37
+ const journalPath = getJournalPath();
38
+ fs.mkdirSync(path.dirname(journalPath), { recursive: true });
39
+ fs.writeFileSync(journalPath, JSON.stringify(journal, null, 2), 'utf8');
40
+ }
41
+
42
+ function entryKey(entry) {
43
+ return `${entry.scope}:${entry.repoRoot || ''}:${entry.path}`;
44
+ }
45
+
46
+ // Re-applying an already-journaled write updates appliedValue/appliedAt but
47
+ // keeps the original priorValue — that's the true pre-apply state undo must
48
+ // restore to, and it must not drift across repeated `apply` runs.
49
+ function upsertJournalEntry(journal, entry) {
50
+ const key = entryKey(entry);
51
+ const existingIndex = journal.entries.findIndex((e) => entryKey(e) === key);
52
+ if (existingIndex === -1) {
53
+ journal.entries.push(entry);
54
+ return;
55
+ }
56
+ journal.entries[existingIndex] = {
57
+ ...entry,
58
+ priorValue: journal.entries[existingIndex].priorValue,
59
+ };
60
+ }
61
+
62
+ function setNestedValue(target, pathStr, value) {
63
+ const keys = pathStr.split('.');
64
+ let node = target;
65
+ for (let i = 0; i < keys.length - 1; i++) {
66
+ const key = keys[i];
67
+ if (typeof node[key] !== 'object' || node[key] === null) {
68
+ node[key] = {};
69
+ }
70
+ node = node[key];
71
+ }
72
+ node[keys[keys.length - 1]] = value;
73
+ }
74
+
75
+ function deleteNestedKey(target, pathStr) {
76
+ const keys = pathStr.split('.');
77
+ let node = target;
78
+ for (let i = 0; i < keys.length - 1; i++) {
79
+ const key = keys[i];
80
+ if (typeof node[key] !== 'object' || node[key] === null) return;
81
+ node = node[key];
82
+ }
83
+ delete node[keys[keys.length - 1]];
84
+ }
85
+
86
+ function deepEqual(a, b) {
87
+ if (a === b) return true;
88
+ if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
89
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
90
+ if (Array.isArray(a)) {
91
+ if (a.length !== b.length) return false;
92
+ return a.every((v, i) => deepEqual(v, b[i]));
93
+ }
94
+ const aKeys = Object.keys(a);
95
+ const bKeys = Object.keys(b);
96
+ if (aKeys.length !== bKeys.length) return false;
97
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
98
+ }
99
+
100
+ module.exports = {
101
+ getJournalPath,
102
+ loadJournal,
103
+ saveJournal,
104
+ upsertJournalEntry,
105
+ getNestedValue,
106
+ setNestedValue,
107
+ deleteNestedKey,
108
+ deepEqual,
109
+ };