harness-everything 0.18.1 → 0.19.1

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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +3 -2
  4. package/CHANGELOG.md +8 -0
  5. package/README.md +21 -14
  6. package/bin/cli.js +16 -0
  7. package/ci/invariant-routing.test.js +42 -19
  8. package/ci/mechanism-2af-worktree-isolation.test.js +3 -3
  9. package/ci/mechanism-2ag-router-worktree-contract.test.js +1 -0
  10. package/ci/mechanism-2an-workflow-obligations.test.js +1 -0
  11. package/ci/mechanism-2ao-plugin-sync.test.js +163 -0
  12. package/ci/mechanism-33-issues-205-207.test.js +2 -2
  13. package/ci/openai-plugin-package.test.js +6 -6
  14. package/ci/router-workflow-plan.test.js +12 -9
  15. package/ci/workflow-execution-contract.test.js +2 -2
  16. package/docs/architecture.md +15 -15
  17. package/docs/enforcement-locks.md +17 -15
  18. package/docs/philosophy.md +43 -8
  19. package/docs/platform-capabilities.md +22 -2
  20. package/docs/platform-compatibility.json +4 -4
  21. package/docs/routing.md +23 -16
  22. package/docs/troubleshooting.md +8 -0
  23. package/docs/workflow-runtime.md +20 -19
  24. package/environment-detection/SKILL.md +3 -1
  25. package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
  26. package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
  27. package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
  28. package/evals/using-git-worktrees/tasks/positive-trigger.yaml +1 -1
  29. package/find-skills/SKILL.md +20 -29
  30. package/git-commit/SKILL.md +4 -4
  31. package/harness-everything/SKILL.md +16 -17
  32. package/harness-everything/references/triage-and-tiers.md +17 -16
  33. package/harness-everything/scripts/kernel-router-core.js +26 -24
  34. package/harness-everything/scripts/kernel-router.js +8 -8
  35. package/harness-everything/scripts/router-contract-core.js +7 -7
  36. package/harness-everything/scripts/router-contract.js +1 -1
  37. package/hooks/scripts/atomic-commit-check.js +1 -1
  38. package/hooks/scripts/boundary-guard.js +2 -2
  39. package/hooks/scripts/depth-guard.js +5 -6
  40. package/hooks/scripts/subagent-scope-guard.js +2 -2
  41. package/hooks/scripts/workflow-gate.js +1 -1
  42. package/install-cognitive-os/SKILL.md +3 -3
  43. package/opencode-plugin/plugin.json +1 -1
  44. package/package.json +4 -2
  45. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  46. package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +1 -1
  47. package/plugins/harness-everything/hooks/scripts/boundary-guard.js +2 -2
  48. package/plugins/harness-everything/hooks/scripts/depth-guard.js +5 -6
  49. package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +2 -2
  50. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +1 -1
  51. package/plugins/harness-everything/plugin.json +1 -1
  52. package/plugins/harness-everything/scripts/lib/advisory-text.js +19 -17
  53. package/plugins/harness-everything/skills/environment-detection/SKILL.md +3 -1
  54. package/plugins/harness-everything/skills/find-skills/SKILL.md +20 -29
  55. package/plugins/harness-everything/skills/git-commit/SKILL.md +4 -4
  56. package/plugins/harness-everything/skills/harness-everything/SKILL.md +16 -17
  57. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +17 -16
  58. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +26 -24
  59. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +8 -8
  60. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +7 -7
  61. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract.js +1 -1
  62. package/plugins/harness-everything/skills/install-cognitive-os/SKILL.md +3 -3
  63. package/plugins/harness-everything/skills/rewrite-commits/SKILL.md +2 -2
  64. package/plugins/harness-everything/skills/to-tickets/SKILL.md +2 -2
  65. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +6 -6
  66. package/rewrite-commits/SKILL.md +2 -2
  67. package/scripts/lib/advisory-text.js +19 -17
  68. package/scripts/plugin-sync.js +465 -0
  69. package/scripts/plugin-sync.ps1 +5 -0
  70. package/scripts/plugin-sync.sh +5 -0
  71. package/to-tickets/SKILL.md +2 -2
  72. package/using-git-worktrees/SKILL.md +6 -6
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Synchronize the native Harness Everything plugin on Claude Code and Codex.
6
+ *
7
+ * This is deliberately separate from installer.js: installer.js manages the
8
+ * portable skills layout, while this command talks to each host's native
9
+ * marketplace/plugin CLI. The installed-state branch is intentionally
10
+ * explicit: an existing plugin is updated, never silently re-added.
11
+ */
12
+
13
+ const { spawnSync } = require('child_process');
14
+
15
+ const DEFAULTS = Object.freeze({
16
+ marketplace: 'harness-everything',
17
+ plugin: 'harness-everything',
18
+ repository: process.env.HARNESS_PLUGIN_REPOSITORY || 'dyphn1/Harness-everything',
19
+ ref: process.env.HARNESS_PLUGIN_REF || 'main'
20
+ });
21
+
22
+ function defaultRunner(command, args) {
23
+ const result = spawnSync(command, args, {
24
+ encoding: 'utf8',
25
+ stdio: ['ignore', 'pipe', 'pipe'],
26
+ windowsHide: true
27
+ });
28
+ return {
29
+ status: result.status === null ? 1 : result.status,
30
+ stdout: result.stdout || '',
31
+ stderr: result.stderr || '',
32
+ error: result.error || null
33
+ };
34
+ }
35
+
36
+ function shellQuote(value) {
37
+ const text = String(value);
38
+ return /^[A-Za-z0-9_./:@%+=,-]+$/.test(text) ? text : JSON.stringify(text);
39
+ }
40
+
41
+ function commandLine(command, args) {
42
+ return [command, ...args].map(shellQuote).join(' ');
43
+ }
44
+
45
+ function parseJsonOutput(output) {
46
+ const text = String(output || '').trim();
47
+ if (!text) return null;
48
+ try {
49
+ return JSON.parse(text);
50
+ } catch (_) {
51
+ // Some host versions print a short status line before one-line JSON.
52
+ const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
53
+ for (let index = lines.length - 1; index >= 0; index--) {
54
+ try {
55
+ return JSON.parse(lines[index]);
56
+ } catch (__) {
57
+ // Keep looking for the machine-readable line.
58
+ }
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function collectObjects(value, out = [], keyHint = '') {
65
+ if (Array.isArray(value)) {
66
+ value.forEach((entry, index) => collectObjects(entry, out, String(index)));
67
+ } else if (value && typeof value === 'object') {
68
+ out.push({ value, keyHint });
69
+ for (const [key, child] of Object.entries(value)) collectObjects(child, out, key);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function collectStrings(value, out = []) {
75
+ if (typeof value === 'string') out.push(value);
76
+ else if (Array.isArray(value)) value.forEach(entry => collectStrings(entry, out));
77
+ else if (value && typeof value === 'object') Object.values(value).forEach(entry => collectStrings(entry, out));
78
+ return out;
79
+ }
80
+
81
+ function exactString(value, expected) {
82
+ return typeof value === 'string' && value.trim() === expected;
83
+ }
84
+
85
+ function findMarketplace(payload, marketplace) {
86
+ if (collectStrings(payload).some(value => exactString(value, marketplace))) return true;
87
+ return collectObjects(payload).some(({ value }) => {
88
+ const candidates = [
89
+ value.name,
90
+ value.marketplace,
91
+ value.marketplaceName,
92
+ value.slug,
93
+ value.id,
94
+ value.source && value.source.name,
95
+ value.source && value.source.id
96
+ ];
97
+ return candidates.some(candidate => exactString(candidate, marketplace));
98
+ });
99
+ }
100
+
101
+ function findPlugin(payload, plugin, marketplace) {
102
+ const pluginId = `${plugin}@${marketplace}`;
103
+ return collectStrings(payload).some(value => value.trim() === pluginId) || collectObjects(payload).some(({ value, keyHint }) => {
104
+ const identifiers = [value.id, value.pluginId, value.identifier, keyHint];
105
+ if (identifiers.some(candidate => exactString(candidate, pluginId))) return true;
106
+ const names = [value.name, value.plugin, value.pluginName];
107
+ if (!names.some(candidate => exactString(candidate, plugin))) return false;
108
+ const sources = [
109
+ value.marketplace,
110
+ value.marketplaceName,
111
+ value.source && value.source.name,
112
+ value.source && value.source.id
113
+ ].filter(Boolean);
114
+ return sources.length === 0 || sources.some(source => exactString(source, marketplace));
115
+ });
116
+ }
117
+
118
+ function escapeRegExp(value) {
119
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
120
+ }
121
+
122
+ function textHasMarketplace(output, marketplace) {
123
+ const expression = new RegExp(`(^|[\\s"'])${escapeRegExp(marketplace)}($|[\\s"'@])`, 'm');
124
+ return expression.test(String(output || ''));
125
+ }
126
+
127
+ function textHasPlugin(output, plugin, marketplace) {
128
+ const text = String(output || '');
129
+ if (text.includes(`${plugin}@${marketplace}`)) return true;
130
+ const expression = new RegExp(`(^|[\\s"'])${escapeRegExp(plugin)}($|[\\s"'@])`, 'm');
131
+ return expression.test(text);
132
+ }
133
+
134
+ function resultError(result) {
135
+ const detail = [result && result.stderr, result && result.stdout, result && result.error && result.error.message]
136
+ .map(value => String(value || '').trim())
137
+ .find(Boolean);
138
+ return detail || `exit status ${result && result.status !== undefined ? result.status : 'unknown'}`;
139
+ }
140
+
141
+ function createContext(options = {}) {
142
+ const runner = options.runner || defaultRunner;
143
+ const log = options.log || (message => console.log(message));
144
+ const warn = options.warn || (message => console.warn(message));
145
+ const context = {
146
+ ...DEFAULTS,
147
+ ...options,
148
+ runner,
149
+ log,
150
+ warn,
151
+ dryRun: Boolean(options.dryRun),
152
+ commandAvailable: options.commandAvailable || (command => {
153
+ const result = runner(command, ['--version'], { probe: true });
154
+ return result && result.status === 0 && !result.error;
155
+ })
156
+ };
157
+ context.repositorySource = options.repositorySource || (
158
+ /^https?:\/\//.test(context.repository) || /^git@/.test(context.repository)
159
+ ? context.repository
160
+ : `https://github.com/${context.repository}.git`
161
+ );
162
+ return context;
163
+ }
164
+
165
+ function invoke(context, command, args, { mutate = false, label = command } = {}) {
166
+ const rendered = commandLine(command, args);
167
+ if (mutate && context.dryRun) {
168
+ context.log(`[dry-run] ${rendered}`);
169
+ return { status: 0, stdout: '', stderr: '', dryRun: true };
170
+ }
171
+ const result = context.runner(command, args, { label });
172
+ if (!result || result.status !== 0) {
173
+ context.warn(`[failed] ${rendered}\n ${resultError(result)}`);
174
+ }
175
+ return result || { status: 1, stdout: '', stderr: 'runner returned no result' };
176
+ }
177
+
178
+ function query(context, command, args) {
179
+ return context.runner(command, args, { query: true }) || { status: 1, stdout: '', stderr: 'runner returned no result' };
180
+ }
181
+
182
+ function listState(context, { command, jsonArgs, plainArgs, detectJson, detectText }) {
183
+ const jsonResult = query(context, command, jsonArgs);
184
+ if (jsonResult.status === 0) {
185
+ const payload = parseJsonOutput(jsonResult.stdout);
186
+ if (payload !== null) return { known: true, value: detectJson(payload), result: jsonResult };
187
+ }
188
+
189
+ const plainResult = query(context, command, plainArgs);
190
+ if (plainResult.status === 0 && String(plainResult.stdout || '').trim()) {
191
+ return { known: true, value: detectText(plainResult.stdout), result: plainResult };
192
+ }
193
+
194
+ return {
195
+ known: false,
196
+ value: false,
197
+ error: `${resultError(jsonResult)}; fallback: ${resultError(plainResult)}`
198
+ };
199
+ }
200
+
201
+ function queryClaudeMarketplace(context) {
202
+ return listState(context, {
203
+ command: 'claude',
204
+ jsonArgs: ['plugin', 'marketplace', 'list', '--json'],
205
+ plainArgs: ['plugin', 'marketplace', 'list'],
206
+ detectJson: payload => findMarketplace(payload, context.marketplace),
207
+ detectText: output => textHasMarketplace(output, context.marketplace)
208
+ });
209
+ }
210
+
211
+ function queryClaudePlugin(context) {
212
+ return listState(context, {
213
+ command: 'claude',
214
+ jsonArgs: ['plugin', 'list', '--json'],
215
+ plainArgs: ['plugin', 'list'],
216
+ detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
217
+ detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
218
+ });
219
+ }
220
+
221
+ function queryCodexMarketplace(context) {
222
+ return listState(context, {
223
+ command: 'codex',
224
+ jsonArgs: ['plugin', 'marketplace', 'list', '--json'],
225
+ plainArgs: ['plugin', 'marketplace', 'list'],
226
+ detectJson: payload => findMarketplace(payload, context.marketplace),
227
+ detectText: output => textHasMarketplace(output, context.marketplace)
228
+ });
229
+ }
230
+
231
+ function queryCodexPlugin(context) {
232
+ const state = listState(context, {
233
+ command: 'codex',
234
+ jsonArgs: ['plugin', 'list', '--marketplace', context.marketplace, '--json'],
235
+ plainArgs: ['plugin', 'list', '--marketplace', context.marketplace],
236
+ detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
237
+ detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
238
+ });
239
+ if (state.known) return state;
240
+
241
+ // Older Codex builds may not accept --marketplace on plugin list. A full
242
+ // list still lets us fail closed while supporting those builds when JSON is
243
+ // available.
244
+ return listState(context, {
245
+ command: 'codex',
246
+ jsonArgs: ['plugin', 'list', '--json'],
247
+ plainArgs: ['plugin', 'list'],
248
+ detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
249
+ detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
250
+ });
251
+ }
252
+
253
+ function ensureClaudeMarketplace(context) {
254
+ const state = queryClaudeMarketplace(context);
255
+ if (!state.known) context.warn(`[notice] Claude marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
256
+ if (!state.known || !state.value) {
257
+ const added = invoke(context, 'claude', [
258
+ 'plugin', 'marketplace', 'add', context.repository,
259
+ '--scope', 'user'
260
+ ], { mutate: true, label: 'claude marketplace add' });
261
+ if (added.status !== 0) return { ok: false, error: resultError(added) };
262
+ }
263
+ return { ok: true };
264
+ }
265
+
266
+ function ensureCodexMarketplace(context) {
267
+ const state = queryCodexMarketplace(context);
268
+ if (!state.known) context.warn(`[notice] Codex marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
269
+ if (!state.known || !state.value) {
270
+ const added = invoke(context, 'codex', [
271
+ 'plugin', 'marketplace', 'add', context.repositorySource,
272
+ '--ref', context.ref,
273
+ '--json'
274
+ ], { mutate: true, label: 'codex marketplace add' });
275
+ if (added.status !== 0) return { ok: false, error: resultError(added) };
276
+ }
277
+ return { ok: true };
278
+ }
279
+
280
+ function refreshClaudeMarketplace(context) {
281
+ const updated = invoke(context, 'claude', [
282
+ 'plugin', 'marketplace', 'update', context.marketplace
283
+ ], { mutate: true, label: 'claude marketplace update' });
284
+ return updated.status === 0
285
+ ? { ok: true }
286
+ : { ok: false, error: resultError(updated) };
287
+ }
288
+
289
+ function refreshCodexMarketplace(context) {
290
+ const upgraded = invoke(context, 'codex', [
291
+ 'plugin', 'marketplace', 'upgrade', context.marketplace,
292
+ '--json'
293
+ ], { mutate: true, label: 'codex marketplace upgrade' });
294
+ return upgraded.status === 0
295
+ ? { ok: true }
296
+ : { ok: false, error: resultError(upgraded) };
297
+ }
298
+
299
+ function verifyPlugin(context, host, queryPlugin) {
300
+ if (context.dryRun) return { ok: true, skipped: true };
301
+ const state = queryPlugin(context);
302
+ if (!state.known) return { ok: false, error: `${host} plugin state could not be verified: ${state.error}` };
303
+ if (!state.value) return { ok: false, error: `${host} reports ${context.plugin}@${context.marketplace} is not installed after synchronization` };
304
+ return { ok: true };
305
+ }
306
+
307
+ function syncClaude(context) {
308
+ if (!context.commandAvailable('claude')) return { host: 'claude', status: 'skipped', reason: 'claude CLI not found' };
309
+ const marketplace = ensureClaudeMarketplace(context);
310
+ if (!marketplace.ok) return { host: 'claude', status: 'failed', error: marketplace.error };
311
+ const installed = queryClaudePlugin(context);
312
+ if (!installed.known) return { host: 'claude', status: 'failed', error: `Claude plugin state could not be determined: ${installed.error}` };
313
+ const refreshed = refreshClaudeMarketplace(context);
314
+ if (!refreshed.ok) return { host: 'claude', status: 'failed', error: refreshed.error };
315
+
316
+ const action = installed.value ? 'update' : 'install';
317
+ const result = invoke(context, 'claude', [
318
+ 'plugin', action, `${context.plugin}@${context.marketplace}`,
319
+ '--scope', 'user', '--yes', '--json'
320
+ ], { mutate: true, label: `claude plugin ${action}` });
321
+ if (result.status !== 0) return { host: 'claude', status: 'failed', error: resultError(result) };
322
+
323
+ const verification = verifyPlugin(context, 'Claude', queryClaudePlugin);
324
+ if (!verification.ok) return { host: 'claude', status: 'failed', error: verification.error };
325
+ return { host: 'claude', status: 'synchronized', action };
326
+ }
327
+
328
+ function syncCodex(context) {
329
+ if (!context.commandAvailable('codex')) return { host: 'codex', status: 'skipped', reason: 'codex CLI not found' };
330
+ const marketplace = ensureCodexMarketplace(context);
331
+ if (!marketplace.ok) return { host: 'codex', status: 'failed', error: marketplace.error };
332
+ const installed = queryCodexPlugin(context);
333
+ if (!installed.known) return { host: 'codex', status: 'failed', error: `Codex plugin state could not be determined: ${installed.error}` };
334
+
335
+ let action;
336
+ let result;
337
+ if (installed.value) {
338
+ // Codex currently refreshes an installed marketplace plugin through the
339
+ // marketplace upgrade command. Do not fall back to `plugin add` here:
340
+ // an installed plugin must take the update path.
341
+ action = 'update';
342
+ result = refreshCodexMarketplace(context);
343
+ if (result.ok) result = { status: 0, stdout: '', stderr: '' };
344
+ else result = { status: 1, stdout: '', stderr: result.error };
345
+ } else {
346
+ action = 'install';
347
+ result = refreshCodexMarketplace(context);
348
+ if (result.ok) {
349
+ result = invoke(context, 'codex', [
350
+ 'plugin', 'add', `${context.plugin}@${context.marketplace}`, '--json'
351
+ ], { mutate: true, label: 'codex plugin install' });
352
+ } else {
353
+ result = { status: 1, stdout: '', stderr: result.error };
354
+ }
355
+ }
356
+ if (result.status !== 0) return { host: 'codex', status: 'failed', error: resultError(result) };
357
+
358
+ const verification = verifyPlugin(context, 'Codex', queryCodexPlugin);
359
+ if (!verification.ok) return { host: 'codex', status: 'failed', error: verification.error };
360
+ return { host: 'codex', status: 'synchronized', action };
361
+ }
362
+
363
+ function parseArgs(argv) {
364
+ const options = { host: 'all', dryRun: false, json: false };
365
+ for (let index = 0; index < argv.length; index++) {
366
+ const arg = argv[index];
367
+ if (arg === '--help' || arg === '-h') options.help = true;
368
+ else if (arg === '--dry-run') options.dryRun = true;
369
+ else if (arg === '--json') options.json = true;
370
+ else if (arg === '--host') {
371
+ options.host = argv[++index];
372
+ if (!options.host) throw new Error('--host requires all, claude, or codex');
373
+ } else if (arg === '--repository') {
374
+ options.repository = argv[++index];
375
+ if (!options.repository) throw new Error('--repository requires a repository or URL');
376
+ } else if (arg === '--ref') {
377
+ options.ref = argv[++index];
378
+ if (!options.ref) throw new Error('--ref requires a git ref');
379
+ } else {
380
+ throw new Error(`Unknown option: ${arg}`);
381
+ }
382
+ }
383
+ options.host = String(options.host).toLowerCase();
384
+ if (!['all', 'claude', 'codex'].includes(options.host)) throw new Error(`Unsupported host: ${options.host}`);
385
+ return options;
386
+ }
387
+
388
+ function help() {
389
+ return `Usage: node scripts/plugin-sync.js [options]
390
+
391
+ Synchronize the native Harness Everything plugin on installed host CLIs.
392
+ Existing installations take the explicit update/upgrade path; absent
393
+ installations take the install path.
394
+
395
+ Options:
396
+ --host <all|claude|codex> Limit synchronization to one host (default: all)
397
+ --repository <repo|url> Marketplace source (default: dyphn1/Harness-everything)
398
+ --ref <git-ref> Codex marketplace ref (default: main)
399
+ --dry-run Detect state and print mutating commands
400
+ --json Print a machine-readable summary
401
+ -h, --help Show this help
402
+ `;
403
+ }
404
+
405
+ function sync(options, dependencies = {}) {
406
+ const context = createContext({ ...options, ...dependencies });
407
+ const hosts = context.host === 'all' ? ['claude', 'codex'] : [context.host];
408
+ const results = hosts.map(host => host === 'claude' ? syncClaude(context) : syncCodex(context));
409
+ const summary = {
410
+ repository: context.repository,
411
+ marketplace: context.marketplace,
412
+ plugin: context.plugin,
413
+ dryRun: context.dryRun,
414
+ results,
415
+ synchronized: results.filter(result => result.status === 'synchronized').length,
416
+ skipped: results.filter(result => result.status === 'skipped').length,
417
+ failed: results.filter(result => result.status === 'failed').length
418
+ };
419
+ return summary;
420
+ }
421
+
422
+ function main(argv = process.argv.slice(2)) {
423
+ let options;
424
+ try {
425
+ options = parseArgs(argv);
426
+ } catch (error) {
427
+ console.error(`[Error] ${error.message}`);
428
+ console.error(help());
429
+ return 2;
430
+ }
431
+ if (options.help) {
432
+ console.log(help());
433
+ return 0;
434
+ }
435
+
436
+ const summary = sync(options);
437
+ if (options.json) console.log(JSON.stringify(summary, null, 2));
438
+ else {
439
+ for (const result of summary.results) {
440
+ if (result.status === 'synchronized') console.log(`[ok] ${result.host}: ${result.action}`);
441
+ else if (result.status === 'skipped') console.log(`[skip] ${result.host}: ${result.reason}`);
442
+ else console.error(`[fail] ${result.host}: ${result.error}`);
443
+ }
444
+ if (summary.synchronized === 0 && summary.failed === 0) {
445
+ console.error('No supported host CLI was found. Install Claude Code and/or Codex, then retry.');
446
+ }
447
+ }
448
+ return summary.failed > 0 || summary.synchronized === 0 ? 1 : 0;
449
+ }
450
+
451
+ if (require.main === module) process.exitCode = main();
452
+
453
+ module.exports = {
454
+ DEFAULTS,
455
+ commandLine,
456
+ createContext,
457
+ findMarketplace,
458
+ findPlugin,
459
+ main,
460
+ parseArgs,
461
+ parseJsonOutput,
462
+ sync,
463
+ textHasMarketplace,
464
+ textHasPlugin
465
+ };
@@ -0,0 +1,5 @@
1
+ $ErrorActionPreference = 'Stop'
2
+
3
+ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
4
+ & node (Join-Path $scriptDir 'plugin-sync.js') @args
5
+ exit $LASTEXITCODE
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
+ exec node "$SCRIPT_DIR/plugin-sync.js" "$@"
@@ -4,7 +4,7 @@ description: Break a to-spec feature-spec, a plan, or a conversation into tracer
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.12.0
7
+ version: 0.19.1
8
8
  ---
9
9
 
10
10
  # To Tickets
@@ -33,7 +33,7 @@ Cuts a plan or spec into tracer-bullet vertical slices, each declaring its block
33
33
  ## Process
34
34
 
35
35
  1. Resolve path: script check (Exit 0 = configured location) → workspace convention dirs → platform fallback (`.claude`/`.github`/`.cursor` + `/harness-everything/tickets/`).
36
- 2. Gather context: prefer a referenced to-spec spec — design-audit verification MANDATORY first, else refuse; no spec is fine.
36
+ 2. Gather context: **SHOULD** use a referenced to-spec spec when available — design-audit verification is MANDATORY first, else refuse; no spec is fine.
37
37
  3. Explore codebase (optional): evidence-cited findings only; prefactor first.
38
38
  4. Draft vertical slices: complete path through every layer, demoable alone, one context window each; declare blocking edges.
39
39
  5. Quiz the user on granularity and edges until approved (MANDATORY gate).
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: using-git-worktrees
3
- description: Use when starting feature work needing isolation or before implementation plans - recommends an isolated workspace via native tools or git worktree fallback
3
+ description: Use when work needs isolation or before major implementation plans - resolves workspace isolation via native tools or git worktree fallback, with an explicit degraded fallback when isolation is unavailable
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.16.1
7
+ version: 0.19.1
8
8
  ---
9
9
 
10
10
  # Using Git Worktrees
@@ -13,9 +13,9 @@ Use native worktree support first; raw Git is the fallback.
13
13
 
14
14
  ## Contract
15
15
 
16
- - **Major workflow guidance** — Tier 3 / Fable-class engineering is safer in a linked worktree before broad source/artifact mutation. Reuse one or create one when practical.
17
- - **Ordinary mode** — isolation-needing feature work may honor an explicit user preference to stay in place.
18
- - Never nest a worktree inside an already isolated worktree.
16
+ - **Tier 3 / Fable (MUST resolve)** — before broad source/artifact mutation, reuse/create a verified linked worktree. If isolation is unavailable or the user explicitly chooses to stay in place, MUST report an explicit degraded fallback/risk before continuing.
17
+ - **Ordinary mode (SHOULD)** — use isolation when it materially reduces collision/risk; explicit user preference may keep work in place.
18
+ - **MUST NOT** nest a worktree inside an already isolated worktree.
19
19
 
20
20
  ## Workflow
21
21
 
@@ -28,7 +28,7 @@ git rev-parse --show-superproject-working-tree
28
28
  ```
29
29
  Different git/common dirs and no superproject => already isolated; continue to Step 2. A submodule is a normal repo for this check.
30
30
 
31
- **Step 1 — Create/enter**: prefer native `EnterWorktree`, `/worktree`, or `--worktree`. Otherwise use `git worktree add "$path" -b "$BRANCH_NAME"`. Prefer an already-ignored or external/sibling path. If creation/entry fails, report it and continue only with explicit awareness of the isolation risk.
31
+ **Step 1 — Create/enter**: use native `EnterWorktree`, `/worktree`, or `--worktree` first; otherwise use `git worktree add "$path" -b "$BRANCH_NAME"`. Use an already-ignored or external/sibling path when possible. If creation/entry fails, MUST surface the degraded isolation disposition before continuing.
32
32
 
33
33
  **Step 2 — Setup**: install project dependencies as needed.
34
34