instar 1.3.475 → 1.3.477

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 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAq3CH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA8C;IAC3E,cAAc,EAAE,UAAU;IAC1B,gBAAgB,EAAE,eAAe;IACjC,mBAAmB,EAAE,UAAU;IAC/B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;CAC/B,CAAC;AAmxGF,gFAAgF;AAChF,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,CAAC;AAClD,mDAAmD;AACnD,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAC/C,sCAAsC;AACtC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,kEAAkE;AAClE,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAq3CH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA8C;IAC3E,cAAc,EAAE,UAAU;IAC1B,gBAAgB,EAAE,eAAe;IACjC,mBAAmB,EAAE,UAAU;IAC/B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;CAC/B,CAAC;AA6xGF,gFAAgF;AAChF,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,CAAC;AAClD,mDAAmD;AACnD,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAC/C,sCAAsC;AACtC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,kEAAkE;AAClE,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.475",
3
+ "version": "1.3.477",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,6 +31,15 @@ import { fileURLToPath } from 'node:url';
31
31
  import { checkEli16Overview, MIN_ELI16_CHARS } from './eli16-overview-check.mjs';
32
32
  import { verifyProposalDerivedRunbooks } from '../skills/instar-dev/scripts/verify-proposal-derived-runbook.mjs';
33
33
  import { classifyTier, decideRequirementSet } from './lib/classify-tier.mjs';
34
+ import { recognizeConvergence } from './lib/convergence-recognition.mjs';
35
+
36
+ // Report-Backed Converging Audit (docs/specs/CONVERGING-AUDIT-DEFAULT.md, Part B).
37
+ // The precommit reads NO config file and runs pre-compile, so it cannot import
38
+ // the TS config loader. The flag specReview.requireConvergenceReport is threaded
39
+ // in as an ENV VAR by the .husky/pre-commit hook, mirroring the existing in-file
40
+ // INSTAR_DEV_ALLOW_ORPHAN_DEFERRALS pattern. When unset, the new report-backing
41
+ // branch never runs → byte-identical to today's precommit behavior.
42
+ const REQUIRE_CONVERGENCE_REPORT = process.env.INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT === '1';
34
43
 
35
44
  const __filename = fileURLToPath(import.meta.url);
36
45
  const __dirname = path.dirname(__filename);
@@ -521,10 +530,34 @@ if (!specFmMatch) {
521
530
  );
522
531
  }
523
532
  const specFm = specFmMatch[1];
524
- const convergenceMatch = specFm.match(/^\s*review-convergence\s*:\s*["']?([^"'\n]+)/m);
525
- const approvedMatch = specFm.match(/^\s*approved\s*:\s*(true|"true"|'true')/m);
526
533
 
527
- if (!convergenceMatch) {
534
+ // Convergence / approval / report recognition is factored into a pure,
535
+ // dependency-free module (scripts/lib/convergence-recognition.mjs) that a unit
536
+ // test cross-checks against the TS validator's `isConvergenceTagPresent` —
537
+ // keeping the two gates in agreement (CONVERGING-AUDIT-DEFAULT.md, Part C). The
538
+ // report-existence input depends on the spec's slug + the env flag, computed
539
+ // just below; the recognizer does no I/O of its own.
540
+
541
+ // Slug → report path, matching StageTransitionValidator's derivation exactly.
542
+ const specSlugMatch = specFm.match(/^\s*slug\s*:\s*["']?([a-z0-9][a-z0-9-]{0,63})["']?\s*$/m);
543
+ const specSlug = specSlugMatch ? specSlugMatch[1] : '';
544
+ const convergenceReportRel = specSlug
545
+ ? path.join('docs/specs/reports', `${specSlug}-convergence.md`)
546
+ : '';
547
+ // Only probe the filesystem when the report requirement is actually on. With
548
+ // the flag unset, reportExists stays false but is never consulted (the
549
+ // recognizer's reportBacked is vacuously true), so this branch is byte-inert.
550
+ const convergenceReportExists =
551
+ REQUIRE_CONVERGENCE_REPORT && convergenceReportRel
552
+ ? fs.existsSync(path.resolve(ROOT, convergenceReportRel))
553
+ : false;
554
+
555
+ const recognition = recognizeConvergence(specFm, {
556
+ requireReport: REQUIRE_CONVERGENCE_REPORT,
557
+ reportExists: convergenceReportExists,
558
+ });
559
+
560
+ if (!recognition.converged) {
528
561
  blockCommit(
529
562
  inScopeFiles,
530
563
  [
@@ -534,7 +567,7 @@ if (!convergenceMatch) {
534
567
  );
535
568
  }
536
569
 
537
- if (!approvedMatch) {
570
+ if (!recognition.approved) {
538
571
  blockCommit(
539
572
  inScopeFiles,
540
573
  [
@@ -545,6 +578,35 @@ if (!approvedMatch) {
545
578
  );
546
579
  }
547
580
 
581
+ // ── Report-backing (Part B — dark, env-gated) ──
582
+ // When INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT=1, a converged + approved spec must
583
+ // ALSO have its converging-audit report on disk (proving the audit RAN, not just
584
+ // that a tag was added). This brings the precommit UP to the formal validator's
585
+ // strictness (which requires the report unconditionally). With the env unset,
586
+ // recognition.reportBacked is always true and this branch never blocks.
587
+ if (!recognition.reportBacked) {
588
+ blockCommit(
589
+ inScopeFiles,
590
+ [
591
+ `Spec ${spec} is tagged review-convergence + approved, but its converging-audit`,
592
+ `report is missing: ${convergenceReportRel || '(spec has no valid slug to locate a report)'}`,
593
+ '',
594
+ 'INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT is on (specReview.requireConvergenceReport:',
595
+ 'true) — a convergence tag without its report can fake convergence. The report is',
596
+ 'the audit\'s proof-of-work; run /spec-converge to produce it (Phase 5 writes',
597
+ `docs/specs/reports/<slug>-convergence.md), then commit. To turn this requirement`,
598
+ 'off, set specReview.requireConvergenceReport: false in your instar config.',
599
+ ].join('\n'),
600
+ );
601
+ }
602
+
603
+ // ── Part D: surface cross-model-review depth (observe-only, never blocks) ──
604
+ // The converging audit records how much external (cross-model) review actually
605
+ // ran. Surfacing it here makes the audit's depth visible to the operator/agent
606
+ // reading the gate output, without ever gating on it (Signal vs. Authority).
607
+ const crossModelMatch = specFm.match(/^\s*cross-model-review\s*:\s*["']?([^"'\n]+)/m);
608
+ const crossModelReview = crossModelMatch ? crossModelMatch[1].trim() : 'not-recorded';
609
+
548
610
  // ─── Step 7: ELI16 overview verification ─────────────────────────────────
549
611
  // Every approved spec must ship with a plain-English ELI16 overview. The
550
612
  // overview is the entry point for any reader who has to make a real decision
@@ -824,7 +886,9 @@ if (!promotionGateResult.ok) {
824
886
  assertFrameworkGenerality(inScopeFiles, validTrace.trace);
825
887
 
826
888
  console.error(
827
- `[instar-dev-precommit] OK — trace ${path.basename(validTrace.entry.file)} covers ${inScopeFiles.length} in-scope file(s), artifact ${validTrace.trace.artifactPath} verified, spec ${spec} is converged + approved, ELI16 overview ${eli16Rel} present (${eli16Result.charCount} chars), promotion-gate: ${promotionGateResult.reason}.`,
889
+ `[instar-dev-precommit] OK — trace ${path.basename(validTrace.entry.file)} covers ${inScopeFiles.length} in-scope file(s), artifact ${validTrace.trace.artifactPath} verified, spec ${spec} is converged + approved` +
890
+ `${REQUIRE_CONVERGENCE_REPORT ? ` + report-backed (${convergenceReportRel})` : ''}` +
891
+ ` [cross-model: ${crossModelReview}], ELI16 overview ${eli16Rel} present (${eli16Result.charCount} chars), promotion-gate: ${promotionGateResult.reason}.`,
828
892
  );
829
893
  process.exit(0);
830
894
 
@@ -0,0 +1,126 @@
1
+ /**
2
+ * convergence-recognition.mjs — the PRECOMMIT gate's pure recognizer for a
3
+ * spec's convergence / approval / report-backing state.
4
+ *
5
+ * Spec: docs/specs/CONVERGING-AUDIT-DEFAULT.md (Part C — gate consistency).
6
+ *
7
+ * WHY a separate pure module: the precommit (scripts/instar-dev-precommit.js)
8
+ * runs PRE-COMPILE and CANNOT import the TS StageTransitionValidator across the
9
+ * compile boundary, so the two gates cannot share a single source of truth in
10
+ * code. Instead, the precommit's recognition logic is factored HERE, into a
11
+ * tiny dependency-free function, and a unit test
12
+ * (tests/unit/convergence-gate-consistency.test.ts) feeds the SAME fixture
13
+ * table to both this recognizer and the validator's `isConvergenceTagPresent`
14
+ * + report logic, asserting they agree. A drift between the two gates fails CI.
15
+ *
16
+ * This module does NO I/O: callers pass the frontmatter text and the
17
+ * report-existence boolean. It is the precommit-side mirror of the validator's
18
+ * pure predicate — same convergence-tag rule (non-empty string OR boolean
19
+ * true), same report-backing rule (report must exist when required).
20
+ */
21
+
22
+ /**
23
+ * Recognize the convergence tag in a `review-convergence` frontmatter VALUE.
24
+ *
25
+ * Mirror of StageTransitionValidator.isConvergenceTagPresent: a non-empty
26
+ * string (the ISO timestamp the converging-audit tooling writes) OR boolean
27
+ * `true` (the legacy/hand-added form) counts as present. Empty string, false,
28
+ * null/undefined, and any other type do NOT.
29
+ *
30
+ * @param {unknown} value
31
+ * @returns {boolean}
32
+ */
33
+ export function isConvergenceTagPresent(value) {
34
+ if (value === true) return true;
35
+ if (typeof value === 'string' && value.trim().length > 0) return true;
36
+ return false;
37
+ }
38
+
39
+ /**
40
+ * Parse the raw `review-convergence` value out of a YAML frontmatter BLOCK
41
+ * (the text between the `---` fences, NOT including the fences). Mirrors the
42
+ * precommit's existing lenient regex exactly so recognition stays identical.
43
+ *
44
+ * Returns the matched string value (quotes stripped, trimmed) when the line is
45
+ * present, or `undefined` when the key is absent. Note: this returns a STRING
46
+ * for both `review-convergence: true` and `review-convergence: "<ts>"` — the
47
+ * precommit's regex captures the literal token; `isConvergenceTagPresent`
48
+ * treats any non-empty captured token as present, which matches the validator's
49
+ * acceptance of both the boolean-true and timestamp-string forms.
50
+ *
51
+ * @param {string} frontmatterText
52
+ * @returns {string | undefined}
53
+ */
54
+ export function parseConvergenceValue(frontmatterText) {
55
+ const m = String(frontmatterText).match(
56
+ /^\s*review-convergence\s*:\s*["']?([^"'\n]+)/m,
57
+ );
58
+ if (!m) return undefined;
59
+ return m[1].trim();
60
+ }
61
+
62
+ /**
63
+ * Parse whether the spec carries an `approved: true` tag (precommit's regex).
64
+ *
65
+ * @param {string} frontmatterText
66
+ * @returns {boolean}
67
+ */
68
+ export function isApprovedTagPresent(frontmatterText) {
69
+ return /^\s*approved\s*:\s*(true|"true"|'true')/m.test(String(frontmatterText));
70
+ }
71
+
72
+ /**
73
+ * The precommit's verdict for "is this spec converged + approved (+ report-backed
74
+ * when required)?" — the same question the formal validator answers, expressed
75
+ * over frontmatter text + a report-existence boolean.
76
+ *
77
+ * @param {string} frontmatterText — the YAML frontmatter block (between the `---` fences).
78
+ * @param {{ requireReport?: boolean, reportExists?: boolean }} [opts]
79
+ * requireReport — true when INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT=1.
80
+ * reportExists — whether docs/specs/reports/<slug>-convergence.md exists.
81
+ * @returns {{ converged: boolean, approved: boolean, reportBacked: boolean,
82
+ * accepted: boolean, reason: string }}
83
+ * - converged: the convergence tag is present.
84
+ * - approved: the approved tag is present.
85
+ * - reportBacked: the report requirement is satisfied (vacuously true when
86
+ * requireReport is false).
87
+ * - accepted: converged AND approved AND reportBacked — the precommit's
88
+ * Step-6 verdict for a tier-2/3 spec.
89
+ * - reason: a short machine-stable reason for the verdict.
90
+ */
91
+ export function recognizeConvergence(frontmatterText, opts = {}) {
92
+ const requireReport = opts.requireReport === true;
93
+ const reportExists = opts.reportExists === true;
94
+
95
+ const convergenceValue = parseConvergenceValue(frontmatterText);
96
+ const converged = isConvergenceTagPresent(convergenceValue);
97
+ const approved = isApprovedTagPresent(frontmatterText);
98
+ const reportBacked = !requireReport || reportExists;
99
+
100
+ let reason;
101
+ if (!converged) reason = 'convergence-tag-missing';
102
+ else if (!approved) reason = 'approved-tag-missing';
103
+ else if (!reportBacked) reason = 'convergence-report-missing';
104
+ else reason = 'accepted';
105
+
106
+ const accepted = converged && approved && reportBacked;
107
+ return { converged, approved, reportBacked, accepted, reason };
108
+ }
109
+
110
+ /**
111
+ * Convenience: the precommit-side answer to "is this spec CONVERGED?" in the
112
+ * exact sense the consistency test compares against the validator's converged
113
+ * verdict (convergence tag present AND report-backed when required). Approval
114
+ * is a separate downstream gate in BOTH paths, so it is NOT folded in here.
115
+ *
116
+ * @param {string} frontmatterText
117
+ * @param {{ requireReport?: boolean, reportExists?: boolean }} [opts]
118
+ * @returns {boolean}
119
+ */
120
+ export function isSpecConverged(frontmatterText, opts = {}) {
121
+ const requireReport = opts.requireReport === true;
122
+ const reportExists = opts.reportExists === true;
123
+ const converged = isConvergenceTagPresent(parseConvergenceValue(frontmatterText));
124
+ const reportBacked = !requireReport || reportExists;
125
+ return converged && reportBacked;
126
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * throwaway-identity.mjs — mint genuinely-distinct, readable throwaway email
3
+ * identities for live-integration test harnesses (Slack, Discord, …).
4
+ *
5
+ * Backed by the mail.tm public disposable-mailbox API. Each minted inbox is a
6
+ * real, distinct, readable address — so a live test can use N of them as N
7
+ * GENUINELY DISTINCT principals (distinct addresses → distinct provider user
8
+ * IDs) with ZERO real accounts. This is the autonomous half of the
9
+ * test-identity provisioning the Live Integration Security-Test Harness needs;
10
+ * the only step it does NOT cover is an anti-bot signup CAPTCHA at workspace/
11
+ * account creation, which is a deliberate human-verification control (see
12
+ * docs/specs/JUDGMENT-PERMISSION-LIVE-RUN-RUNBOOK.md).
13
+ *
14
+ * Pure helpers (extractCode/extractLink/matchMessage) and HTTP helpers with an
15
+ * injectable `fetchImpl` so the unit test runs fully hermetic (no network).
16
+ *
17
+ * CLI: scripts/throwaway-identity.mjs (mint | wait). This file is the importable
18
+ * library; the CLI is a thin wrapper.
19
+ */
20
+
21
+ const DEFAULT_API_BASE = 'https://api.mail.tm';
22
+
23
+ /** Extract the first verification code (default: a 6-digit run) from a message body. */
24
+ export function extractCode(text, { pattern = /\b(\d{6})\b/ } = {}) {
25
+ if (typeof text !== 'string') return null;
26
+ const m = text.match(pattern);
27
+ return m ? (m[1] ?? m[0]) : null;
28
+ }
29
+
30
+ /** Extract the first URL (optionally matching a substring/regex) from a message body. */
31
+ export function extractLink(text, { match } = {}) {
32
+ if (typeof text !== 'string') return null;
33
+ const urls = text.match(/https?:\/\/[^\s"'<>)\]]+/g) || [];
34
+ if (!match) return urls[0] ?? null;
35
+ const test = match instanceof RegExp ? (u) => match.test(u) : (u) => u.includes(match);
36
+ return urls.find(test) ?? null;
37
+ }
38
+
39
+ /** Does a mail.tm message summary match the caller's filter (subject/from substring or regex)? */
40
+ export function matchMessage(msg, { subject, from } = {}) {
41
+ if (!msg) return false;
42
+ const subjOk = matchField(msg.subject, subject);
43
+ const fromAddr = (msg.from && (msg.from.address || msg.from.name)) || '';
44
+ const fromOk = matchField(fromAddr, from);
45
+ return subjOk && fromOk;
46
+ }
47
+
48
+ function matchField(value, filter) {
49
+ if (filter === undefined || filter === null) return true;
50
+ const v = String(value ?? '');
51
+ return filter instanceof RegExp ? filter.test(v) : v.toLowerCase().includes(String(filter).toLowerCase());
52
+ }
53
+
54
+ function memberArray(json) {
55
+ // mail.tm is a Hydra/JSON-LD API: collections live under "hydra:member".
56
+ if (Array.isArray(json)) return json;
57
+ if (json && Array.isArray(json['hydra:member'])) return json['hydra:member'];
58
+ return [];
59
+ }
60
+
61
+ async function jsonFetch(fetchImpl, url, opts) {
62
+ const res = await fetchImpl(url, opts);
63
+ const text = await res.text();
64
+ let body = null;
65
+ try { body = text ? JSON.parse(text) : null; } catch { body = null; }
66
+ if (!res.ok) {
67
+ const detail = (body && (body['hydra:description'] || body.message || body.detail)) || text.slice(0, 200);
68
+ throw new Error(`mail.tm ${opts?.method || 'GET'} ${url} → ${res.status}: ${detail}`);
69
+ }
70
+ return body;
71
+ }
72
+
73
+ /**
74
+ * Mint a fresh throwaway inbox: pick a domain, create the account, get a token.
75
+ * Returns { address, password, token, accountId }. `rand` is injectable so the
76
+ * test is deterministic and the script never calls Math.random in a workflow.
77
+ */
78
+ export async function createInbox({
79
+ fetchImpl = fetch,
80
+ apiBase = DEFAULT_API_BASE,
81
+ localPart,
82
+ rand = () => Math.floor(Math.random() * 1e10).toString(36),
83
+ } = {}) {
84
+ const domains = memberArray(await jsonFetch(fetchImpl, `${apiBase}/domains`));
85
+ const domain = domains.find((d) => d.isActive !== false)?.domain || domains[0]?.domain;
86
+ if (!domain) throw new Error('mail.tm: no available domain');
87
+ const local = localPart || `echo-${rand()}`;
88
+ const address = `${local}@${domain}`;
89
+ const password = `Echo!${rand()}A9`;
90
+ const account = await jsonFetch(fetchImpl, `${apiBase}/accounts`, {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({ address, password }),
94
+ });
95
+ const tokenResp = await jsonFetch(fetchImpl, `${apiBase}/token`, {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ address, password }),
99
+ });
100
+ if (!tokenResp?.token) throw new Error('mail.tm: token request returned no token');
101
+ return { address, password, token: tokenResp.token, accountId: account?.id ?? null };
102
+ }
103
+
104
+ /** List message summaries in the inbox (most-recent first, per mail.tm). */
105
+ export async function listMessages(token, { fetchImpl = fetch, apiBase = DEFAULT_API_BASE } = {}) {
106
+ const body = await jsonFetch(fetchImpl, `${apiBase}/messages`, {
107
+ headers: { Authorization: `Bearer ${token}` },
108
+ });
109
+ return memberArray(body);
110
+ }
111
+
112
+ /** Fetch one message's full body (text + html). */
113
+ export async function getMessage(token, id, { fetchImpl = fetch, apiBase = DEFAULT_API_BASE } = {}) {
114
+ return jsonFetch(fetchImpl, `${apiBase}/messages/${id}`, {
115
+ headers: { Authorization: `Bearer ${token}` },
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Poll the inbox until a message matches the filter, then return its full body.
121
+ * `sleep` and `now` are injectable so the test runs without real timers/clock.
122
+ * Throws on timeout. Returns the full message (with .text / .html / .subject).
123
+ */
124
+ export async function waitForMessage(token, {
125
+ subject,
126
+ from,
127
+ timeoutMs = 120_000,
128
+ intervalMs = 3_000,
129
+ fetchImpl = fetch,
130
+ apiBase = DEFAULT_API_BASE,
131
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
132
+ now = () => Date.now(),
133
+ } = {}) {
134
+ const deadline = now() + timeoutMs;
135
+ // First iteration runs immediately (no upfront sleep).
136
+ for (let first = true; ; first = false) {
137
+ if (!first) {
138
+ if (now() >= deadline) {
139
+ throw new Error(`waitForMessage: timed out after ${timeoutMs}ms (subject=${subject ?? '*'} from=${from ?? '*'})`);
140
+ }
141
+ await sleep(intervalMs);
142
+ }
143
+ const summaries = await listMessages(token, { fetchImpl, apiBase });
144
+ const hit = summaries.find((m) => matchMessage(m, { subject, from }));
145
+ if (hit) return getMessage(token, hit.id, { fetchImpl, apiBase });
146
+ if (first && now() >= deadline) {
147
+ throw new Error(`waitForMessage: timed out after ${timeoutMs}ms (subject=${subject ?? '*'} from=${from ?? '*'})`);
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * throwaway-identity.mjs — CLI for the disposable-identity helper (lib/throwaway-identity.mjs).
4
+ *
5
+ * node scripts/throwaway-identity.mjs mint
6
+ * → mints a fresh inbox; prints JSON { address, password, token, accountId }.
7
+ *
8
+ * node scripts/throwaway-identity.mjs wait <token> [--subject S] [--from F]
9
+ * [--code | --link [MATCH]] [--timeout MS]
10
+ * → polls the inbox until a matching message arrives, then prints either
11
+ * the full message JSON, or (with --code/--link) just the extracted value.
12
+ *
13
+ * Use for live-integration test-identity provisioning (the autonomous half — an
14
+ * anti-bot signup CAPTCHA at account creation is the human handoff).
15
+ */
16
+ import {
17
+ createInbox,
18
+ waitForMessage,
19
+ extractCode,
20
+ extractLink,
21
+ } from './lib/throwaway-identity.mjs';
22
+
23
+ function parseFlags(args) {
24
+ const out = { _: [] };
25
+ for (let i = 0; i < args.length; i++) {
26
+ const a = args[i];
27
+ if (a === '--code') out.code = true;
28
+ else if (a === '--link') { out.link = true; if (args[i + 1] && !args[i + 1].startsWith('--')) out.linkMatch = args[++i]; }
29
+ else if (a === '--subject') out.subject = args[++i];
30
+ else if (a === '--from') out.from = args[++i];
31
+ else if (a === '--timeout') out.timeout = parseInt(args[++i], 10);
32
+ else out._.push(a);
33
+ }
34
+ return out;
35
+ }
36
+
37
+ async function main() {
38
+ const [cmd, ...rest] = process.argv.slice(2);
39
+ const flags = parseFlags(rest);
40
+
41
+ if (cmd === 'mint') {
42
+ const inbox = await createInbox();
43
+ process.stdout.write(JSON.stringify(inbox) + '\n');
44
+ return;
45
+ }
46
+
47
+ if (cmd === 'wait') {
48
+ const token = flags._[0];
49
+ if (!token) { console.error('usage: throwaway-identity.mjs wait <token> [--subject S] [--from F] [--code|--link [MATCH]] [--timeout MS]'); process.exit(1); }
50
+ const msg = await waitForMessage(token, {
51
+ subject: flags.subject,
52
+ from: flags.from,
53
+ timeoutMs: flags.timeout ?? 120_000,
54
+ });
55
+ const text = msg.text || msg.html?.join?.('\n') || (Array.isArray(msg.html) ? msg.html.join('\n') : msg.html) || '';
56
+ if (flags.code) {
57
+ const code = extractCode(text);
58
+ if (!code) { console.error('no verification code found in message'); process.exit(1); }
59
+ process.stdout.write(code + '\n');
60
+ } else if (flags.link) {
61
+ const link = extractLink(text, flags.linkMatch ? { match: flags.linkMatch } : {});
62
+ if (!link) { console.error('no link found in message'); process.exit(1); }
63
+ process.stdout.write(link + '\n');
64
+ } else {
65
+ process.stdout.write(JSON.stringify({ subject: msg.subject, from: msg.from, text }) + '\n');
66
+ }
67
+ return;
68
+ }
69
+
70
+ console.error('usage: throwaway-identity.mjs <mint | wait>');
71
+ process.exit(1);
72
+ }
73
+
74
+ import { pathToFileURL } from 'node:url';
75
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
76
+ main().catch((err) => { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); });
77
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-10T18:44:18.495Z",
5
- "instarVersion": "1.3.475",
4
+ "generatedAt": "2026-06-10T19:33:00.272Z",
5
+ "instarVersion": "1.3.477",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,33 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Added a standalone test utility — `scripts/throwaway-identity.mjs` (+ `scripts/lib/`) — that
9
+ mints genuinely-distinct, readable throwaway email inboxes via the mail.tm public
10
+ disposable-mailbox API and extracts verification codes/links from them. It's the autonomous
11
+ half of provisioning distinct test identities for the Live Integration Security-Test Harness
12
+ (Slack today, any integration later): N distinct inboxes → N genuinely-distinct principals,
13
+ zero real accounts. No runtime code, gate, or config is touched.
14
+
15
+ ## What to Tell Your User
16
+
17
+ Nothing changes in how the agent runs. This is a developer/test tool. It lets the agent set
18
+ up several genuinely-different throwaway test users (and read their email) on its own, so a
19
+ live integration test can use real distinct identities without anyone hand-creating email
20
+ accounts. The only step it intentionally leaves to a human is passing an anti-bot CAPTCHA at
21
+ workspace creation.
22
+
23
+ ## Summary of New Capabilities
24
+
25
+ - `scripts/throwaway-identity.mjs mint` — create a fresh readable throwaway inbox.
26
+ - `scripts/throwaway-identity.mjs wait <token>` — await an email and extract its code/link.
27
+ - `scripts/lib/throwaway-identity.mjs` — importable helper (HTTP injectable for hermetic tests).
28
+
29
+ ## Evidence
30
+
31
+ - 15 hermetic unit tests (`tests/unit/throwaway-identity.test.ts`) — pure extractors + the
32
+ mint / poll-until-match / timeout flow, with fetch + clock injected (no network).
33
+ - Live CLI smoke minted a real inbox + token. `tsc --noEmit` clean.
@@ -0,0 +1,63 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Spec #4 of the cartographer-conformance project — **fixes a real defect in the
9
+ convergence gate and makes the report-backed converging audit the structural
10
+ default** (dark, off by default).
11
+
12
+ The defect: the formal initiative gate (`StageTransitionValidator`) required the
13
+ `review-convergence` frontmatter tag to equal the boolean `true`, but the actual
14
+ converging-audit tooling writes an ISO **timestamp string**. A timestamp is not the
15
+ boolean `true`, so the formal gate **rejected every properly-converged spec** at the
16
+ `spec-drafted → spec-converged` transition — quietly broken. This change adds a pure
17
+ `isConvergenceTagPresent` predicate that accepts the canonical timestamp (and still
18
+ the legacy boolean), used as the single definition of "the tag is present."
19
+
20
+ On top of the defect repair, a new default-off flag `specReview.requireConvergenceReport`
21
+ makes the converging audit's **report** (the proof it actually ran) a requirement of
22
+ the commit-time gate when enabled — so "converged" can mean "the audit ran and left a
23
+ report," not just "a tag was added." The commit-time gate reads the flag via an
24
+ environment variable (it runs before compilation and reads no config), exported from
25
+ config by the husky hook; when unset, the commit-time gate behaves exactly as before.
26
+ The formal gate's existing report requirement is left unconditional, so enabling the
27
+ flag brings the two gates into agreement — and a cross-gate consistency test makes it
28
+ impossible for them to drift apart. Whether an outside model also reviewed a spec is
29
+ surfaced in the gate's diagnostics, never required.
30
+
31
+ ## What to Tell Your User
32
+
33
+ - **The review gate was quietly broken — now it's fixed and means something**: "The
34
+ check that confirms a design was properly reviewed before it ships had a bug — it was
35
+ looking for the wrong marker, so it actually rejected every correctly-reviewed
36
+ design. I repaired it, and added an opt-in setting that makes 'reviewed' require the
37
+ review's report to actually exist, not just a tag. It's off by default, so nothing in
38
+ your current workflow changes until you turn it on."
39
+
40
+ ## Summary of New Capabilities
41
+
42
+ | Capability | How to Use |
43
+ |-----------|-----------|
44
+ | Report-backed convergence gate | `specReview.requireConvergenceReport: true` in config (opt-in; off by default) |
45
+ | Correct convergence-tag recognition | automatic — the formal gate now accepts the canonical timestamp tag |
46
+
47
+ ## Evidence
48
+
49
+ - **Before:** a spec converged via the real tooling carries
50
+ `review-convergence: "2026-06-10T…"`. Feeding it to the formal gate returned
51
+ `CONVERGENCE_TAG_MISSING` because `"2026-06-10T…" !== true` — the `spec-drafted →
52
+ spec-converged` transition could not advance any real spec. (The existing tests only
53
+ ever fed the boolean `true`, which is why the defect shipped unnoticed.)
54
+ - **After:** the same timestamp-tagged + reported spec advances through the real
55
+ `POST /projects/:id/advance` route (new integration test); a timestamp-tagged spec
56
+ with a MISSING report still returns `CONVERGENCE_REPORT_MISSING` (the existing,
57
+ unchanged check); and a new predicate test covers timestamp / boolean / empty /
58
+ missing.
59
+ - **Back-compat verified end-to-end:** a test runs the real commit-time gate script —
60
+ with the flag unset, a timestamp-tagged + approved spec with no report commits
61
+ cleanly (identical to today); with the flag on and no report, it is blocked; with
62
+ the report present, it commits. This PR's own commit passes through the modified gate
63
+ with the flag off.