great-cto 3.75.9 → 3.75.10

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "great-cto",
3
3
  "description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 72 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
4
- "version": "3.75.9",
4
+ "version": "3.75.10",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -200,6 +200,29 @@ function expectedCandidate(files) {
200
200
  const inventory = files.map(file => ({ path: file.path, size: file.bytes.length, sha256: hash(file.bytes), mode: file.mode })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
201
201
  return hash(JSON.stringify(inventory));
202
202
  }
203
+ /** What npm wrote against what was admitted: names the first difference, keeps no bytes. */
204
+ function candidateDifference(files, packageRoot) {
205
+ const actual = new Map(packageSnapshot(packageRoot).files.map(file => [file.path, file]));
206
+ const differing = files.filter(file => { const found = actual.get(file.path); return !found || found.sha256 !== hash(file.bytes) || found.mode !== file.mode; });
207
+ const first = differing[0], found = first && actual.get(first.path);
208
+ return { differing: differing.length, unexpected: [...actual.keys()].filter(name => !files.some(file => file.path === name)).length, ...(first ? { path: first.path, expectedMode: first.mode.toString(8), actualMode: found ? found.mode.toString(8) : null, sameBytes: !!found && found.sha256 === hash(first.bytes) } : {}) };
209
+ }
210
+ /** The result of a failed step, bounded: the journal explains the rollback and stays small. */
211
+ function stepFailure(step, result) {
212
+ return { step, status: result?.status ?? null, signal: result?.signal ?? null, error: result?.error?.code || result?.error?.message || null,
213
+ stdout: String(result?.stdout || '').slice(-500), stderr: String(result?.stderr || '').slice(-2000) };
214
+ }
215
+ /** npm applies the caller's umask to the files it unpacks, and the admitted inventory
216
+ * includes modes. A private umask must not turn identical bytes into a failed activation. */
217
+ function withPackageUmask(action) {
218
+ const previous = process.umask(0o022);
219
+ try {
220
+ return action();
221
+ }
222
+ finally {
223
+ process.umask(previous);
224
+ }
225
+ }
203
226
  function privateInstallationState(stateRoot, packageRoot, { recovering = false } = {}) {
204
227
  const root = canonicalDirectory(stateRoot, { create: true, privateMode: true });
205
228
  const installationId = hash(packageRoot), dir = path.join(root, installationId);
@@ -303,9 +326,13 @@ function validJournal(journal, installation, state) {
303
326
  fail('invalid-recovery-selection');
304
327
  return journal;
305
328
  }
306
- function rollback(installation, state, journal, context) {
329
+ function rollback(installation, state, journal, context, reason = null, detail = null) {
307
330
  try {
308
331
  validJournal(journal, installation, state);
332
+ if (reason) {
333
+ journal.failure = reason;
334
+ journal.failureDetail = detail;
335
+ }
309
336
  const backup = path.join(state.dir, 'transactions', journal.transactionId, 'previous/package');
310
337
  const snapshot = packageSnapshot(backup);
311
338
  if (snapshot.digest !== journal.previous.packageDigest)
@@ -314,7 +341,7 @@ function rollback(installation, state, journal, context) {
314
341
  if (stateMatches(current, journal.previous)) {
315
342
  journal.state = 'ROLLED_BACK';
316
343
  writeRecord(state.journalPath, journal);
317
- return outcome('ROLLED_BACK', context);
344
+ return outcome('ROLLED_BACK', context, reason || journal.failure);
318
345
  }
319
346
  if (!journal.written || !stateMatches(current, journal.written))
320
347
  fail('recovery-target-drift-or-unobserved-write');
@@ -358,7 +385,7 @@ function rollback(installation, state, journal, context) {
358
385
  journal.state = 'ROLLED_BACK';
359
386
  journal.recovery = 'RESTORED_PREEXISTING_OPERATOR_STATE';
360
387
  writeRecord(state.journalPath, journal);
361
- return outcome('ROLLED_BACK', context);
388
+ return outcome('ROLLED_BACK', context, reason || journal.failure);
362
389
  }
363
390
  catch (error) {
364
391
  journal.state = 'RECOVERY_REQUIRED';
@@ -467,7 +494,7 @@ export function activateNpmRelease(admission, options) {
467
494
  if (finalArtifact.sha256 !== artifact.sha256 || hash(readRegular(activationArchive, { max: 128 * 1024 * 1024 })) !== artifact.sha256)
468
495
  fail('artifact-changed-at-activation');
469
496
  const args = ['install', '--global', activationArchive, '--ignore-scripts', '--offline', '--prefix', installation.prefix, '--package-lock=false', '--no-save', '--audit=false', '--fund=false'];
470
- const result = run(npmPath, args, { cwd: tx, env, encoding: 'utf8', timeout: 180000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
497
+ const result = withPackageUmask(() => run(npmPath, args, { cwd: tx, env, encoding: 'utf8', timeout: 180000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }));
471
498
  try {
472
499
  journal.written = targetState(installation);
473
500
  }
@@ -476,15 +503,20 @@ export function activateNpmRelease(admission, options) {
476
503
  }
477
504
  phase('ACTIVE');
478
505
  if (result?.error || result?.signal || result?.status !== 0)
479
- return rollback(installation, state, journal, context);
480
- if (!journal.written || journal.written.filesDigest !== expectedCandidate(payload.files))
481
- return rollback(installation, state, journal, context);
506
+ return rollback(installation, state, journal, context, 'npm-install-failed', stepFailure('npm-install', result));
507
+ if (!journal.written)
508
+ return rollback(installation, state, journal, context, 'installed-package-unreadable');
509
+ if (journal.written.filesDigest !== expectedCandidate(payload.files))
510
+ return rollback(installation, state, journal, context, 'installed-files-differ-from-admitted', candidateDifference(payload.files, installation.packageRoot));
482
511
  if (!stateMatches(targetState(installation), journal.written))
483
512
  return outcome('RECOVERY_REQUIRED', context, 'target-drift-after-activation');
484
513
  for (const smokeArgs of [[installation.binaryPath, '--version'], [installation.binaryPath, 'codex-host', 'list']]) {
485
514
  const smoke = run(process.execPath, smokeArgs, { cwd: tx, env, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
486
- if (smoke?.error || smoke?.signal || smoke?.status !== 0 || (smokeArgs.includes('--version') && String(smoke.stdout || '').trim() !== info.version))
487
- return rollback(installation, state, journal, context);
515
+ const step = 'smoke-' + (smokeArgs.includes('--version') ? 'version' : 'codex-host-list');
516
+ if (smoke?.error || smoke?.signal || smoke?.status !== 0)
517
+ return rollback(installation, state, journal, context, step + '-failed', stepFailure(step, smoke));
518
+ if (smokeArgs.includes('--version') && String(smoke.stdout || '').trim() !== info.version)
519
+ return rollback(installation, state, journal, context, 'smoke-version-mismatch', stepFailure(step, smoke));
488
520
  if (!stateMatches(targetState(installation), journal.written))
489
521
  return outcome('RECOVERY_REQUIRED', context, 'target-drift-during-smoke');
490
522
  }
@@ -496,7 +528,7 @@ export function activateNpmRelease(admission, options) {
496
528
  if (journal && error.activationCrash)
497
529
  return outcome('RECOVERY_REQUIRED', context, 'interrupted-activation');
498
530
  if (journal && ['ACTIVATING', 'ACTIVE', 'RECOVERY_REQUIRED'].includes(journal.state)) {
499
- return rollback(installation, state, journal, context);
531
+ return rollback(installation, state, journal, context, error.activationReason || 'activation-check-failed');
500
532
  }
501
533
  if (journal) {
502
534
  journal.state = 'REFUSED';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.75.9",
3
+ "version": "3.75.10",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",