great-cto 3.28.8 → 3.28.9

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 70 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.28.8",
4
+ "version": "3.28.9",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -52,7 +52,7 @@ export const MAX_FILES = 200;
52
52
  * Returns `null` outside a git repository rather than a fabricated receipt: a
53
53
  * receipt that cannot be built must not look like one that matched.
54
54
  */
55
- export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_FILES } = {}) {
55
+ export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_FILES, verdictsDir = null } = {}) {
56
56
  const head = git(['rev-parse', 'HEAD'], cwd)?.trim();
57
57
  if (!head) return null;
58
58
 
@@ -73,7 +73,35 @@ export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_F
73
73
  // Which files the change touches. `--diff-filter=d` drops deletions: a file
74
74
  // that is gone cannot have a blob sha, and its absence is already visible in
75
75
  // the map as a missing key.
76
- const ref = base || mergeBase(cwd) || 'HEAD';
76
+ // What the change is measured against. The merge-base with the default branch
77
+ // is right on a feature branch, and wrong on the branch itself: once the work is
78
+ // on main, or there is no upstream at all, the merge-base IS HEAD, and a stage
79
+ // that committed before recording its verdict gets a change set holding only
80
+ // uncommitted leftovers. A live run on 2026-09-14 did exactly that — senior-dev
81
+ // committed its fix, the receipt named two logs and a report, and the
82
+ // independent judge, shown no code, correctly said nothing was implemented.
83
+ //
84
+ // So when the caller hands in the project's verdicts, the change starts where
85
+ // the previous stage stood (taskBase), unless the merge-base is closer.
86
+ // `base_from` records which, so a reader can tell what the judge was shown.
87
+ let ref, baseFrom;
88
+ if (base) {
89
+ ref = base; baseFrom = 'explicit';
90
+ } else {
91
+ const tb = verdictsDir ? taskBase(cwd, { verdictsDir, head }) : null;
92
+ const mb = mergeBase(cwd);
93
+ const mbBelow = mb && mb !== head ? mb : null;
94
+ if (tb && mbBelow) {
95
+ ref = isAncestor(cwd, mbBelow, tb) ? tb : mbBelow;
96
+ baseFrom = ref === tb ? 'task' : 'merge-base';
97
+ } else if (tb) {
98
+ ref = tb; baseFrom = 'task';
99
+ } else if (mbBelow) {
100
+ ref = mbBelow; baseFrom = 'merge-base';
101
+ } else {
102
+ ref = mb || 'HEAD'; baseFrom = 'head';
103
+ }
104
+ }
77
105
  const names = [
78
106
  ...(git(['diff', '--name-only', '--diff-filter=d', ref], cwd) ?? '')
79
107
  .split('\n').map((s) => s.trim()).filter(Boolean),
@@ -90,7 +118,7 @@ export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_F
90
118
  if (blob) files[p] = blob;
91
119
  }
92
120
 
93
- return { head, dirty, base: ref, files, ...(truncated ? { truncated: true } : {}) };
121
+ return { head, dirty, base: ref, base_from: baseFrom, files, ...(truncated ? { truncated: true } : {}) };
94
122
  }
95
123
 
96
124
  /**
@@ -105,6 +133,50 @@ export function fileDigest(cwd, path) {
105
133
  return out ? out.trim() : null;
106
134
  }
107
135
 
136
+ /** Is `ancestor` in the history of `descendant`? */
137
+ function isAncestor(cwd, ancestor, descendant) {
138
+ return git(['merge-base', '--is-ancestor', ancestor, descendant], cwd) !== null;
139
+ }
140
+
141
+ /**
142
+ * Where the current stage's work began: the commit the previous stage stood on.
143
+ *
144
+ * The newest verdict in `verdictsDir` that carries a receipt names that commit as
145
+ * `receipt.head`. It is a base only if it is still in this history and is not
146
+ * HEAD itself — a previous stage recorded at HEAD means nothing has been
147
+ * committed since, and an earlier verdict would attribute another stage's work
148
+ * to this one. A recorded head that is not an ancestor (rebased away, or from a
149
+ * different clone) is skipped for the next newest.
150
+ *
151
+ * Null when there is no such commit; the caller falls back to the merge-base.
152
+ */
153
+ export function taskBase(cwd, { verdictsDir, head = null, read = readFileSync, list = null } = {}) {
154
+ if (!verdictsDir) return null;
155
+ const now = head || git(['rev-parse', 'HEAD'], cwd)?.trim();
156
+ if (!now) return null;
157
+ let names;
158
+ try { names = (list || fsModule.readdirSync)(verdictsDir).filter((f) => f.endsWith('.log')); }
159
+ catch { return null; }
160
+ const recorded = [];
161
+ for (const f of names) {
162
+ let text;
163
+ try { text = read(join(verdictsDir, f), 'utf8'); } catch { continue; }
164
+ for (const line of String(text).split('\n')) {
165
+ if (!line.trim()) continue;
166
+ let rec;
167
+ try { rec = JSON.parse(line); } catch { continue; }
168
+ const h = rec?.receipt?.head;
169
+ if (typeof h === 'string' && /^[0-9a-f]{7,40}$/.test(h)) recorded.push({ ts: String(rec.ts || ''), head: h });
170
+ }
171
+ }
172
+ recorded.sort((a, b) => b.ts.localeCompare(a.ts));
173
+ for (const { head: h } of recorded) {
174
+ if (h === now) return null;
175
+ if (isAncestor(cwd, h, now)) return h;
176
+ }
177
+ return null;
178
+ }
179
+
108
180
  /** The fork point from the default branch, or null when there isn't one. */
109
181
  export function mergeBase(cwd) {
110
182
  for (const branch of ['origin/main', 'main', 'origin/master', 'master']) {
@@ -301,7 +373,10 @@ function requireFs() {
301
373
  if (import.meta.url === `file://${process.argv[1]}`) {
302
374
  const argv = process.argv.slice(2);
303
375
  if (argv.includes('--emit')) {
304
- const r = treeReceipt(process.cwd());
376
+ // `--verdicts <dir>`: measure the change from where the previous stage stood.
377
+ const vi = argv.indexOf('--verdicts');
378
+ const verdictsDir = vi >= 0 && argv[vi + 1] ? argv[vi + 1] : null;
379
+ const r = treeReceipt(process.cwd(), { verdictsDir });
305
380
  process.stdout.write(r ? JSON.stringify(r) : '');
306
381
  process.exit(r ? 0 : 1);
307
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.28.8",
3
+ "version": "3.28.9",
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",