@remits/remits-cli 0.1.135 → 0.1.136

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.
package/index.js CHANGED
@@ -6,16 +6,16 @@
6
6
  - L22 Runtime Bootstrap And Shared State
7
7
  - L119 Sessions, Account Resolution, And Production Guards
8
8
  - L603 Local State, Workspaces, And Verification Evidence
9
- - L2474 Account Repos, Guide Sync, And Platform Repo
10
- - L3282 Component Discovery And HTTP Logging
11
- - L3923 Skill Delivery And TOC Resolution
12
- - L4233 Auth And Component Staging
13
- - L4848 Component Summaries, Status, And Sync Gates
14
- - L6917 Branches, Promotion, Commit, And Test Runs
15
- - L8600 Tokens, Tools, Verification, And Config
16
- - L9980 Service Dashboard And WebSocket Listener
17
- - L12101 Agent And Ticket Workflows
18
- - L15573 Help, Auto Update, And Command Dispatch
9
+ - L2624 Account Repos, Guide Sync, And Platform Repo
10
+ - L3432 Component Discovery And HTTP Logging
11
+ - L4073 Skill Delivery And TOC Resolution
12
+ - L4383 Auth And Component Staging
13
+ - L4998 Component Summaries, Status, And Sync Gates
14
+ - L7067 Branches, Promotion, Commit, And Test Runs
15
+ - L8750 Tokens, Tools, Verification, And Config
16
+ - L10177 Service Dashboard And WebSocket Listener
17
+ - L12298 Agent And Ticket Workflows
18
+ - L15888 Help, Auto Update, And Command Dispatch
19
19
  */
20
20
 
21
21
  /*
@@ -626,6 +626,7 @@ function localStatePaths(cwd) {
626
626
  toolResponsesDir: path.join(actorDir, 'tool-responses'),
627
627
  verificationDir: path.join(actorDir, 'verification'),
628
628
  diagnosticsDir: path.join(actorDir, 'diagnostics'),
629
+ evidenceFile: path.join(actorDir, 'evidence.jsonl'),
629
630
  activeVerificationFile: path.join(actorDir, 'verification', 'active'),
630
631
  activeVerificationContextsFile: path.join(actorDir, 'verification', 'active-contexts.json'),
631
632
  currentSessionFile: path.join(actorDir, 'current-session.txt'),
@@ -1999,9 +2000,145 @@ async function printVerificationRunProbe(api, cwd, session, accountId, envelopeI
1999
2000
  return result;
2000
2001
  }
2001
2002
 
2003
+ /**
2004
+ * The always-on evidence trail.
2005
+ *
2006
+ * <p>One world-stamped line per evidence-producing command, written whether or not any verification envelope
2007
+ * exists. This is the answer to "what have I actually run, and in which world?" - the question agents were
2008
+ * opening envelopes to answer, and an envelope is a far heavier instrument than that question needs. It is
2009
+ * per-actor, so two agents in one checkout never read each other's trail, and it is append-only with a bounded
2010
+ * tail so it cannot grow without limit.</p>
2011
+ *
2012
+ * <p>Recording is deliberately decision-free: there is no flag to remember, no state to start, and nothing an
2013
+ * agent can fail to satisfy.</p>
2014
+ */
2015
+ const EVIDENCE_TRAIL_MAX_LINES = 2000;
2016
+
2017
+ function recordEvidenceEntry(cwd, packet, context = {}) {
2018
+ try {
2019
+ const paths = ensureLocalState(cwd);
2020
+ const world = (packet && packet.world && typeof packet.world === 'object') ? packet.world : {};
2021
+ const hasSuccess = packet && Object.prototype.hasOwnProperty.call(packet, 'success');
2022
+ const entry = {
2023
+ ts: new Date().toISOString(),
2024
+ type: packet && packet.type || 'unknown',
2025
+ claim: packet && packet.claim || null,
2026
+ success: hasSuccess ? packet.success : true,
2027
+ pending: packet && packet.pending === true || (hasSuccess && packet.success === null),
2028
+ world: {
2029
+ repoAccountId: world.repoAccountId != null ? world.repoAccountId : context.accountId || null,
2030
+ executionAccountId: world.executionAccountId != null ? world.executionAccountId : (world.accountId != null ? world.accountId : null),
2031
+ targetAccountId: world.targetAccountId != null ? world.targetAccountId : null,
2032
+ dataMode: world.dataMode || context.dataMode || null,
2033
+ branchName: world.branchName || world.gitBranch || context.branchName || null,
2034
+ componentBranch: world.componentBranch || world.variantBranch || null,
2035
+ workspace: world.workspace !== undefined ? world.workspace : (context.workspace !== undefined ? context.workspace : null),
2036
+ sourceLayer: world.sourceLayer || null,
2037
+ laneContentHash: world.laneContentHash || world.stagedOverlayHash || null,
2038
+ host: world.host || context.baseUrl || null
2039
+ },
2040
+ ref: {
2041
+ packetId: packet && packet.packetId || null,
2042
+ taskId: (packet && packet.test && packet.test.taskId) || (packet && packet.status && packet.status.taskId) || null,
2043
+ callId: (packet && packet.tool && packet.tool.callId) || null,
2044
+ responseFile: (packet && packet.tool && packet.tool.responseFile) || null
2045
+ },
2046
+ // Present when an envelope happened to be active. Never required, and never something to chase.
2047
+ envelopeId: context.envelopeId || null,
2048
+ claims: Array.isArray(packet && packet.claims) ? packet.claims : undefined
2049
+ };
2050
+ fs.appendFileSync(paths.evidenceFile, JSON.stringify(entry) + '\n');
2051
+ trimEvidenceTrail(paths.evidenceFile);
2052
+ return entry;
2053
+ } catch (_) {
2054
+ // A trail write must never fail the command it is describing.
2055
+ return null;
2056
+ }
2057
+ }
2058
+
2059
+ function evidenceOutcome(row) {
2060
+ if (row && (row.pending === true || row.success === null)) return 'pending';
2061
+ return row && row.success === false ? 'failed' : 'ok';
2062
+ }
2063
+
2064
+ function evidenceOutcomeLabel(row) {
2065
+ const outcome = evidenceOutcome(row);
2066
+ if (outcome === 'failed') return 'FAILED ';
2067
+ if (outcome === 'pending') return 'PENDING ';
2068
+ return '';
2069
+ }
2070
+
2071
+ function trimEvidenceTrail(file) {
2072
+ try {
2073
+ const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
2074
+ if (lines.length <= EVIDENCE_TRAIL_MAX_LINES) return;
2075
+ atomicWriteFile(file, lines.slice(lines.length - EVIDENCE_TRAIL_MAX_LINES).join('\n') + '\n');
2076
+ } catch (_) { /* best effort */ }
2077
+ }
2078
+
2079
+ function readEvidenceTrail(cwd, limit = 50) {
2080
+ try {
2081
+ const paths = localStatePaths(cwd);
2082
+ if (!fs.existsSync(paths.evidenceFile)) return [];
2083
+ const lines = fs.readFileSync(paths.evidenceFile, 'utf8').split('\n').filter(Boolean);
2084
+ return lines.slice(Math.max(0, lines.length - limit)).map((line) => {
2085
+ try { return JSON.parse(line); } catch (_) { return null; }
2086
+ }).filter(Boolean);
2087
+ } catch (_) {
2088
+ return [];
2089
+ }
2090
+ }
2091
+
2092
+ /**
2093
+ * WHERE a command ran: accounts, lane, branch, workspace. Deliberately NOT `sourceLayer` or
2094
+ * `laneContentHash` - those legitimately differ between two commands in the same world (a stage is `staged`,
2095
+ * the test that follows resolves `db`), so keying the grouping on them split one world into several and made
2096
+ * two entries from the same context look like two different places. They belong on the entry.
2097
+ */
2098
+ function describeEvidenceWorld(world) {
2099
+ const w = world || {};
2100
+ const accounts = w.executionAccountId && String(w.executionAccountId) !== String(w.repoAccountId)
2101
+ ? 'account ' + w.repoAccountId + ' -> runs as ' + w.executionAccountId
2102
+ : 'account ' + (w.repoAccountId == null ? '?' : w.repoAccountId);
2103
+ return [
2104
+ accounts,
2105
+ w.targetAccountId && String(w.targetAccountId) !== String(w.executionAccountId) ? 'targets ' + w.targetAccountId : null,
2106
+ w.dataMode ? w.dataMode + ' lane' : null,
2107
+ w.branchName ? 'branch ' + w.branchName : null,
2108
+ w.componentBranch ? 'components ' + w.componentBranch : null,
2109
+ 'ws:' + (w.workspace || 'shared')
2110
+ ].filter(Boolean).join(' · ');
2111
+ }
2112
+
2113
+ function claimIdsFromFlags(flags) {
2114
+ return []
2115
+ .concat((flags && flags.claim) || [])
2116
+ .concat((flags && flags.claims) || [])
2117
+ .filter(Boolean)
2118
+ .flatMap((value) => String(value).split(','))
2119
+ .map((value) => value.trim())
2120
+ .filter(Boolean);
2121
+ }
2122
+
2123
+ function stampPacketClaims(packet, flags) {
2124
+ const claims = claimIdsFromFlags(flags);
2125
+ if (!claims.length) return packet;
2126
+ if (!Array.isArray(packet.evidenceCategories)) packet.evidenceCategories = [];
2127
+ claims.forEach((claim) => {
2128
+ if (!packet.evidenceCategories.includes(claim)) packet.evidenceCategories.push(claim);
2129
+ });
2130
+ packet.claims = claims;
2131
+ return packet;
2132
+ }
2133
+
2002
2134
  async function appendVerificationPacket(api, cwd, session, accountId, flags, packet, options = {}) {
2003
2135
  const context = activeVerificationContext(cwd, flags, session, accountId);
2004
2136
  const envelopeId = verificationEnvelopeIdForCommand(cwd, flags, context);
2137
+ // The trail is written FIRST and unconditionally. Evidence recording must not depend on an agent having
2138
+ // remembered to start anything: an envelope is an optional verdict on top of this, never the thing that
2139
+ // makes a command's world durable.
2140
+ stampPacketClaims(packet, flags);
2141
+ recordEvidenceEntry(cwd, packet, Object.assign({}, context, { accountId, envelopeId }));
2005
2142
  if (!envelopeId) return null;
2006
2143
  const finalPacket = Object.assign({
2007
2144
  packetId: crypto.randomUUID(),
@@ -2067,7 +2204,7 @@ function printVerificationSummaryLine(env) {
2067
2204
  const health = env.health ? ' health=' + env.health : '';
2068
2205
  const failures = env.currentFailureCount ? ' currentFailures=' + env.currentFailureCount : (env.failedCount ? ' failed=' + env.failedCount : '');
2069
2206
  const required = ' required=' + (env.satisfiedCount || 0) + '/' + (env.requiredCount || 0);
2070
- const noManifest = env.noRequiredEvidence ? ' no-manifest' : '';
2207
+ const noManifest = env.noRequiredEvidence ? ' evidence-log' : '';
2071
2208
  const stale = env.staleCount ? ' stale=' + env.staleCount : '';
2072
2209
  const changed = env.requirementsChangedAfterEvidence ? ' requirements-changed' : '';
2073
2210
  const lane = [env.branchName || 'unknown-branch', env.workspace ? 'ws:' + env.workspace : 'shared', env.dataMode || null, env.sourceLayer || null]
@@ -2084,7 +2221,7 @@ function printVerificationEnvelope(envelope, fallbackEnvelopeId) {
2084
2221
  const evaluation = envelope.evaluation || {};
2085
2222
  if (evaluation.health) console.log('Health:', evaluation.health);
2086
2223
  if (evaluation.noRequiredEvidence) {
2087
- console.log('Required evidence: none declared - evidence packets do not prove a complete acceptance contract.');
2224
+ console.log('Required evidence: none declared - evidence log only; no verdict is outstanding.');
2088
2225
  } else {
2089
2226
  console.log('Required evidence:', (evaluation.satisfiedCount || 0) + '/' + (evaluation.requiredCount || 0));
2090
2227
  }
@@ -2143,7 +2280,7 @@ function compactVerificationEnvelope(envelope, fallbackEnvelopeId) {
2143
2280
  const nextActions = [];
2144
2281
 
2145
2282
  if (evaluation.noRequiredEvidence) {
2146
- nextActions.push('Attach a manifest or required evidence contract; packets are context, not proof of completion.');
2283
+ nextActions.push('No verdict was requested. Leave this as an evidence log, or add `verify claim <id> --text "..."` only if someone needs a checkable verdict.');
2147
2284
  }
2148
2285
  if (failures.length) {
2149
2286
  nextActions.push('Inspect and replace the current failing evidence packet(s).');
@@ -2227,14 +2364,27 @@ function printEnvelopeWarnings(envelope) {
2227
2364
  console.log('Envelope warnings:');
2228
2365
  warnings.forEach((warning) => {
2229
2366
  console.log('- ' + (warning.message || warning.code || JSON.stringify(warning)));
2367
+ if (warning.command) console.log(' ' + warning.command);
2230
2368
  if (Array.isArray(warning.envelopes) && warning.envelopes.length) {
2231
2369
  warning.envelopes.forEach((env) => {
2232
- console.log(' ' + (env.envelopeId || '(no id)') + (env.status ? ' ' + env.status : ''));
2370
+ // Say whether the duplicate is in THIS world. A restart in a genuinely different lane or branch is
2371
+ // sometimes exactly right; a second envelope for the same goal in the same world is the loop.
2372
+ const world = env.world && typeof env.world === 'object'
2373
+ ? Object.keys(env.world).map((key) => key + '=' + env.world[key]).join(' ')
2374
+ : '';
2375
+ console.log(' ' + (env.envelopeId || '(no id)') + (env.status ? ' ' + env.status : '') +
2376
+ (env.sameWorld === true ? ' [same world]' : (world ? ' [' + world + ']' : '')));
2233
2377
  });
2378
+ console.log(' Continue one of those with `remits-cli verify use <id>`, or supersede it once this one supersedes its goal.');
2234
2379
  }
2235
2380
  });
2236
2381
  }
2237
2382
 
2383
+ function manifestClaims(manifest) {
2384
+ const claims = manifest && manifest.claims;
2385
+ return Array.isArray(claims) ? claims.filter(Boolean) : [];
2386
+ }
2387
+
2238
2388
  function manifestRequiredEvidence(manifest = {}) {
2239
2389
  const required = [];
2240
2390
  if (Array.isArray(manifest.requiredEvidence)) required.push(...manifest.requiredEvidence);
@@ -9565,10 +9715,16 @@ async function verifyCommand(flags, subcommand) {
9565
9715
  if (flags.manifest || flags.file) {
9566
9716
  manifest = parseManifestFile(flags.manifest || flags.file);
9567
9717
  }
9568
- if (dataMode === 'prod' && !manifestRequiredEvidence(manifest).length && !flagEnabled(flags['no-contract'])) {
9718
+ // PROD only, and framed as risk rather than as an unmet obligation. An envelope with no contract is an
9719
+ // evidence log, which is the normal and complete mode; warning about it on every lane taught agents that
9720
+ // their own record-keeping was an exam they were failing, and they responded by starting it over.
9721
+ if (dataMode === 'prod' && !manifestRequiredEvidence(manifest).length && !manifestClaims(manifest).length &&
9722
+ !flagEnabled(flags['no-contract'])) {
9569
9723
  console.error('');
9570
- console.error('Warning: starting a prod-data verification envelope with no required evidence contract.');
9571
- console.error('Attach a manifest with requiredEvidence before destructive work, or pass --no-contract when this is intentionally evidence-only.');
9724
+ console.error('Note: this envelope records PRODUCTION-data evidence with no acceptance contract.');
9725
+ console.error('That is fine for investigation. Before destructive or irreversible production work, name what');
9726
+ console.error('must be true so the result is checkable by someone else:');
9727
+ console.error(' remits-cli verify claim <id> --text "what must be true" [--test "<suite>"]');
9572
9728
  console.error('');
9573
9729
  }
9574
9730
  let statusResponse = null;
@@ -9598,12 +9754,17 @@ async function verifyCommand(flags, subcommand) {
9598
9754
  ticketId: flags.ticket,
9599
9755
  account: collectVerificationAccount(cwd, accountId, baseUrl, dataMode),
9600
9756
  source,
9601
- manifest
9757
+ manifest,
9758
+ noContract: flagEnabled(flags['no-contract']) || undefined
9602
9759
  });
9603
9760
  const envelope = response.envelope;
9604
9761
  writeLocalVerificationEnvelope(cwd, envelope);
9605
9762
  writeActiveVerificationEnvelope(cwd, envelope.envelopeId, activeContext);
9606
9763
  console.log('Verification envelope started:', envelope.envelopeId);
9764
+ if (!manifestRequiredEvidence(manifest).length && !manifestClaims(manifest).length) {
9765
+ console.log('Mode: evidence log (no acceptance contract asked for). Commands attach world-stamped evidence;');
9766
+ console.log(' nothing is outstanding. Add `verify claim <id> --text "..."` only if someone needs a verdict.');
9767
+ }
9607
9768
  printLocalCommandContext(cwd, flags, { accountId, branchName, workspace, dataMode });
9608
9769
  printLocalStateWarnings(cwd, flags);
9609
9770
  console.log('Target:', checkoutWorldLine + ', dataMode=' + dataMode +
@@ -9683,6 +9844,40 @@ async function verifyCommand(flags, subcommand) {
9683
9844
  return envelope;
9684
9845
  }
9685
9846
 
9847
+ if (sub === 'claim' || sub === 'claims') {
9848
+ const claimId = flags.id || (flags._ && flags._[2]);
9849
+ const text = flags.text || flags.claim || flags.summary || flags.note;
9850
+ if (!claimId) {
9851
+ throw new Error('Usage: remits-cli verify claim <id> --text "what must be true" [--test "<suite>"] [--packet-type <type>]');
9852
+ }
9853
+ const claim = { id: String(claimId) };
9854
+ if (text) claim.text = String(text);
9855
+ // A claim may name the shape of evidence that proves it, so one line of contract can bind to a suite
9856
+ // instead of needing a second parallel requiredEvidence list kept in sync by hand.
9857
+ const suite = flags.test || flags.suite;
9858
+ if (suite) {
9859
+ claim.packetType = 'test_run';
9860
+ claim.suite = String(suite);
9861
+ if (flags.case || flags.cases) {
9862
+ claim.cases = [].concat(flags.case || []).concat(flags.cases || [])
9863
+ .flatMap((value) => String(value).split(',')).map((value) => value.trim()).filter(Boolean);
9864
+ }
9865
+ } else if (flags['packet-type'] || flags.packetType) {
9866
+ claim.packetType = String(flags['packet-type'] || flags.packetType);
9867
+ }
9868
+ const response = await postVerificationCommand(api, cwd, session, accountId, 'claim', { envelopeId, claim });
9869
+ writeLocalVerificationEnvelope(cwd, response.envelope);
9870
+ const acceptance = (response.envelope || {}).acceptance || {};
9871
+ console.log('Claim recorded on envelope ' + envelopeId + ': ' + claim.id);
9872
+ console.log('Contract now requires ' + (acceptance.requiredEvidence || []).length + ' item(s).');
9873
+ console.log('Prove it by adding --claim ' + claim.id + ' to the command that demonstrates it, e.g.');
9874
+ console.log(' remits-cli verify test --test "<suite>" --claim ' + claim.id);
9875
+ console.log(' remits-cli verify attach --claim ' + claim.id + ' --note "what you observed"');
9876
+ printAcceptanceWarnings(response.envelope);
9877
+ printEnvelopeWarnings(response.envelope);
9878
+ return response.envelope;
9879
+ }
9880
+
9686
9881
  if (sub === 'attach') {
9687
9882
  const artifacts = [];
9688
9883
  let type = 'manual_observation';
@@ -9719,7 +9914,7 @@ async function verifyCommand(flags, subcommand) {
9719
9914
  artifacts,
9720
9915
  evidenceCategories
9721
9916
  };
9722
- const response = await appendVerificationPacket(api, cwd, session, accountId, { 'verify-envelope': envelopeId }, packet);
9917
+ const response = await appendVerificationPacket(api, cwd, session, accountId, Object.assign({}, flags, { 'verify-envelope': envelopeId }), packet);
9723
9918
  console.log('Evidence attached:', (response.packet || packet).packetId);
9724
9919
  return response;
9725
9920
  }
@@ -9730,7 +9925,9 @@ async function verifyCommand(flags, subcommand) {
9730
9925
  const packet = {
9731
9926
  type: sub === 'browser-start' ? 'browser_session' : (sub === 'browser-snapshot' ? 'browser_snapshot' : 'browser_step'),
9732
9927
  success: !flagEnabled(flags.failed),
9733
- claim: flags.claim || flags.label || action,
9928
+ // `--claim` now names a declared acceptance claim id, so the free-text label for a browser step is
9929
+ // `--label`. Reading both from one flag made the id double as prose and the prose double as an id.
9930
+ claim: flags.label || flags.text || action,
9734
9931
  world: buildCommandWorld(null, { accountId, dataMode, branchName, workspace, host: normalizeBaseUrl(baseUrl) }),
9735
9932
  browser: {
9736
9933
  action,
@@ -9743,7 +9940,7 @@ async function verifyCommand(flags, subcommand) {
9743
9940
  evidenceCategories: [sub, action === 'upload' ? 'browser_session.actual_upload' : null, action === 'click' ? 'browser_session.actual_clicks' : null]
9744
9941
  .filter(Boolean)
9745
9942
  };
9746
- const response = await appendVerificationPacket(api, cwd, session, accountId, { 'verify-envelope': envelopeId }, packet);
9943
+ const response = await appendVerificationPacket(api, cwd, session, accountId, Object.assign({}, flags, { 'verify-envelope': envelopeId }), packet);
9747
9944
  console.log('Browser evidence attached:', (response.packet || packet).packetId);
9748
9945
  return response;
9749
9946
  }
@@ -14708,23 +14905,57 @@ function slimActivitySmell(smell) {
14708
14905
  testName: s.testName || null,
14709
14906
  branchName: s.branchName || null,
14710
14907
  workspace: s.workspace || null,
14908
+ packetCount: s.packetCount == null ? null : s.packetCount,
14909
+ requiredCount: s.requiredCount == null ? null : s.requiredCount,
14910
+ openCount: s.openCount == null ? null : s.openCount,
14911
+ command: s.command || null,
14711
14912
  updatedAt: formatTime(s.updatedAtMs || s.updatedAt || s.at || s.createdAt)
14712
14913
  };
14713
14914
  }
14714
14915
 
14916
+ /**
14917
+ * The next COMMAND, not the smell restated. This list previously mapped each signal to its own summary
14918
+ * sentence, so "Next actions" contained no action; and it filtered on severity <= 2, which permanently
14919
+ * excluded `no_required_evidence` - the one signal that explains why an envelope never closes.
14920
+ * A signal that knows its own fix is actionable at any severity.
14921
+ */
14922
+ function buildActivityNextActions(smells) {
14923
+ const seen = new Set();
14924
+ return (smells || [])
14925
+ .filter((smell) => smell && (smell.command || smellSeverityRank(smell) <= 2))
14926
+ .sort((left, right) => {
14927
+ const command = (right.command ? 1 : 0) - (left.command ? 1 : 0);
14928
+ if (command) return command;
14929
+ return smellSeverityRank(left) - smellSeverityRank(right);
14930
+ })
14931
+ .map((smell) => {
14932
+ const title = smell.title || smell.envelopeSummary || smell.testName ||
14933
+ (Array.isArray(smell.workspaces) ? smell.workspaces.join(', ') : null);
14934
+ return {
14935
+ type: smell.type || 'signal',
14936
+ severity: smell.severity || 'info',
14937
+ why: (title ? title + ' — ' : '') + (smell.summary || 'inspect this signal'),
14938
+ command: smell.command || null
14939
+ };
14940
+ })
14941
+ .filter((action) => {
14942
+ // Dedupe on the COMMAND when there is one: two different signals about one envelope that resolve to
14943
+ // the same next move are one next move.
14944
+ const key = action.command ? 'cmd|' + action.command : action.type + '|' + action.why;
14945
+ if (seen.has(key)) return false;
14946
+ seen.add(key);
14947
+ return true;
14948
+ })
14949
+ .slice(0, 6);
14950
+ }
14951
+
14715
14952
  function compactActivityInspect(response) {
14716
14953
  const counts = response.counts || {};
14717
14954
  const smells = sortedActivitySmells(response.smells);
14718
14955
  const lanes = Array.isArray(response.lanes) ? response.lanes : [];
14719
14956
  const envelopes = Array.isArray(response.envelopes) ? response.envelopes : [];
14720
14957
  const testRuns = Array.isArray(response.testRuns) ? response.testRuns : [];
14721
- const nextActions = smells
14722
- .filter((smell) => smellSeverityRank(smell) <= 2)
14723
- .slice(0, 5)
14724
- .map((smell) => {
14725
- const title = smell.title || smell.envelopeSummary || smell.testName;
14726
- return (smell.type || 'signal') + (title ? ' (' + title + ')' : '') + ': ' + (smell.summary || 'inspect this signal');
14727
- });
14958
+ const nextActions = buildActivityNextActions(smells);
14728
14959
 
14729
14960
  return {
14730
14961
  success: response.success,
@@ -14809,7 +15040,14 @@ function printActivityInspectSummary(response) {
14809
15040
  (counts.lanes || 0) + ' lane(s), ' +
14810
15041
  (counts.verificationEnvelopes || 0) + ' envelope(s), ' +
14811
15042
  (counts.testRuns || 0) + ' test run(s), ' +
14812
- (counts.corpusResults || 0) + ' corpus result(s)');
15043
+ (counts.corpusResults || 0) + ' corpus result(s)' +
15044
+ (counts.distinctStagedEntries != null && counts.distinctStagedEntries !== counts.stagedEntries
15045
+ ? ', ' + counts.stagedEntries + ' staged entries across lanes (' + counts.distinctStagedEntries + ' distinct)'
15046
+ : (counts.stagedEntries ? ', ' + counts.stagedEntries + ' staged entries' : '')));
15047
+ if (response.envelopeScope === 'all-data-lanes') {
15048
+ console.log('Note: test runs and corpus results are narrowed to the ' + response.dataMode +
15049
+ ' lane; envelopes are listed across BOTH lanes (stage/sync evidence is lane-independent).');
15050
+ }
14813
15051
 
14814
15052
  const smells = sortedActivitySmells(response.smells);
14815
15053
  console.log('');
@@ -14819,6 +15057,18 @@ function printActivityInspectSummary(response) {
14819
15057
  console.log('Top smells (' + smells.length + '):');
14820
15058
  smells.slice(0, 20).forEach((smell) => {
14821
15059
  console.log(' [' + (smell.severity || 'info') + '] ' + (smell.type || 'signal') + ': ' + smell.summary);
15060
+ // The numbers were in the JSON and not on the line, so twenty signals read as three sentences repeated.
15061
+ const facts = [
15062
+ smell.packetCount != null ? smell.packetCount + ' packet(s)' : null,
15063
+ smell.currentFailureCount != null ? smell.currentFailureCount + ' current failure(s)' : null,
15064
+ smell.requiredCount != null ? smell.requiredCount + ' requirement(s)' : null,
15065
+ smell.openCount != null ? smell.openCount + ' open' : null,
15066
+ smell.recentRuns != null ? smell.recentRuns + ' recent run(s)' : null,
15067
+ smell.failedRuns != null ? smell.failedRuns + ' failed' : null,
15068
+ smell.stagedCount != null ? smell.stagedCount + ' staged' : null,
15069
+ Array.isArray(smell.workspaces) ? smell.workspaces.join(', ') : null
15070
+ ].filter(Boolean);
15071
+ if (facts.length) console.log(' ' + facts.join(' · '));
14822
15072
  const pivots = [
14823
15073
  smell.accountId ? 'account ' + smell.accountId : null,
14824
15074
  smell.laneId ? 'lane ' + smell.laneId : null,
@@ -14831,12 +15081,15 @@ function printActivityInspectSummary(response) {
14831
15081
  if (pivots.length) console.log(' ' + pivots.join(' · '));
14832
15082
  });
14833
15083
  if (smells.length > 20) console.log(' ...' + (smells.length - 20) + ' more; rerun with --json');
14834
- const actions = compactActivityInspect(response).nextActions;
14835
- if (actions.length) {
14836
- console.log('');
14837
- console.log('Next actions:');
14838
- actions.forEach((action) => console.log(' - ' + action));
14839
- }
15084
+ }
15085
+ const actions = buildActivityNextActions(smells);
15086
+ if (actions.length) {
15087
+ console.log('');
15088
+ console.log('Next actions:');
15089
+ actions.forEach((action) => {
15090
+ console.log(' - ' + action.why);
15091
+ if (action.command) console.log(' ' + action.command);
15092
+ });
14840
15093
  }
14841
15094
 
14842
15095
  // "2 agent(s)" with no names is the shape of the question, not the answer: an agent asking who else is in
@@ -14882,8 +15135,14 @@ function printActivityInspectSummary(response) {
14882
15135
  console.log('');
14883
15136
  console.log('Recent verification envelopes:');
14884
15137
  envelopes.slice(0, 8).forEach((env) => {
15138
+ const world = [env.branchName || null, env.workspace ? 'ws:' + env.workspace : null, env.dataMode || null]
15139
+ .filter(Boolean).join('/');
14885
15140
  console.log(' - ' + (env.status || 'unknown') + ' · ' + (env.packetCount || 0) + ' packet(s) · ' +
15141
+ (world ? '[' + world + '] · ' : '') +
14886
15142
  (env.summary || env.envelopeId) + ' · ' + formatTime(env.updatedAtMs || env.lastPacketAtMs || env.createdAt));
15143
+ if (env.noRequiredEvidence) {
15144
+ console.log(' evidence log — no verdict requested; add a claim only if someone needs one: remits-cli verify claim <id> --text "..." --envelope ' + env.envelopeId);
15145
+ }
14887
15146
  if (env.currentFailureCount) console.log(' current failures: ' + env.currentFailureCount);
14888
15147
  });
14889
15148
  }
@@ -14946,6 +15205,62 @@ function listWorkstreamIndexes(cwd) {
14946
15205
  .sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
14947
15206
  }
14948
15207
 
15208
+ async function evidenceCommand(flags) {
15209
+ const cwd = process.cwd();
15210
+ ensureLocalState(cwd);
15211
+ const limit = Math.max(1, Math.min(parseInt(flags.limit || flags.max || '40', 10) || 40, 500));
15212
+ const entries = readEvidenceTrail(cwd, limit);
15213
+ if (flagEnabled(flags.json)) {
15214
+ console.log(JSON.stringify({ success: true, actor: resolveLocalActor().id, count: entries.length, entries }, null, 2));
15215
+ return entries;
15216
+ }
15217
+ const paths = localStatePaths(cwd);
15218
+ console.log('Evidence trail for actor ' + paths.actor.id + ' (' + paths.evidenceFile + ')');
15219
+ if (!entries.length) {
15220
+ console.log('Nothing recorded yet. Every stage, test, token, tool and sync appends one world-stamped line here,');
15221
+ console.log('with no envelope and nothing to start.');
15222
+ return entries;
15223
+ }
15224
+ // Grouped by WORLD, because "what have I proven" is only answerable per world. An agent that ran the same
15225
+ // suite in two lanes has two different facts, and a flat list hides exactly that.
15226
+ const groups = new Map();
15227
+ entries.forEach((entry) => {
15228
+ const key = describeEvidenceWorld(entry.world);
15229
+ if (!groups.has(key)) groups.set(key, []);
15230
+ groups.get(key).push(entry);
15231
+ });
15232
+ groups.forEach((rows, world) => {
15233
+ console.log('');
15234
+ console.log(world);
15235
+ const byClaim = new Map();
15236
+ rows.forEach((row) => {
15237
+ const key = (row.type || 'unknown') + '|' + (row.claim || row.type || 'evidence') + '|' + evidenceOutcome(row);
15238
+ if (!byClaim.has(key)) byClaim.set(key, { row, count: 0 });
15239
+ const bucket = byClaim.get(key);
15240
+ bucket.count += 1;
15241
+ bucket.row = row;
15242
+ });
15243
+ Array.from(byClaim.values())
15244
+ .sort((left, right) => right.count - left.count)
15245
+ .slice(0, 15)
15246
+ .forEach((bucket) => {
15247
+ const row = bucket.row;
15248
+ const layer = row.world && row.world.sourceLayer ? ' source ' + row.world.sourceLayer : '';
15249
+ console.log(' ' + (bucket.count > 1 ? bucket.count + ' x ' : '') +
15250
+ evidenceOutcomeLabel(row) + (row.type || 'unknown') + ': ' + (row.claim || '(no claim)') +
15251
+ ' ' + formatTime(row.ts) + layer +
15252
+ (Array.isArray(row.claims) && row.claims.length ? ' [claims ' + row.claims.join(', ') + ']' : '') +
15253
+ (row.envelopeId ? ' [envelope ' + row.envelopeId.slice(0, 8) + ']' : ''));
15254
+ });
15255
+ if (byClaim.size > 15) console.log(' ...' + (byClaim.size - 15) + ' more distinct entries; --json for all');
15256
+ });
15257
+ console.log('');
15258
+ console.log(entries.length + ' entry(s) shown. This is a log, not a verdict: nothing here is outstanding.');
15259
+ console.log('If someone needs a checkable verdict, start an envelope and name what must be true:');
15260
+ console.log(' remits-cli verify start --summary "..." && remits-cli verify claim <id> --text "..."');
15261
+ return entries;
15262
+ }
15263
+
14949
15264
  async function workstreamCommand(flags, subcommand) {
14950
15265
  const cwd = process.cwd();
14951
15266
  ensureLocalState(cwd);
@@ -16021,6 +16336,9 @@ function printVerifyHelp() {
16021
16336
  console.log(' remits-cli verify use <envelopeId>');
16022
16337
  console.log(' remits-cli verify current');
16023
16338
  console.log(' remits-cli verify clear [--all]');
16339
+ console.log(' remits-cli verify claim <id> --text "what must be true" [--test "<suite>"] [--packet-type TYPE]');
16340
+ console.log(' # declares a contract in ONE command when someone needs a verdict; no-contract envelopes are evidence logs');
16341
+ console.log(' # prove it by adding --claim <id> to any verify test/token/tool/stage/attach command');
16024
16342
  console.log(' remits-cli verify show [--envelope ID] [--summary|--compact] [--json]');
16025
16343
  console.log(' remits-cli verify status [--envelope ID]');
16026
16344
  console.log(' remits-cli verify report [--envelope ID]');
@@ -16142,8 +16460,9 @@ async function main() {
16142
16460
  console.log(' remits-cli tool --name <toolName> [--base-url URL] [--account-id ID] [--as-account ID] [--target-account ID] [--branch BRANCH] [--input \"{...}\"|--input-file file.json] [--data-mode test|prod] [--scope self|children|hierarchy] [--account-ids 1,2,3] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
16143
16461
  console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
16144
16462
  console.log(' remits-cli workspace [show|use <name>|use --auto|clear] # isolate staging when several agents share a repo');
16145
- console.log(' remits-cli verify start --summary "..." [--manifest file.json] # start a verification envelope');
16146
- console.log(' remits-cli verify [current|list|use|clear|show|status|report|attach|abandon|supersede|test|token|stage|sync|tool]');
16463
+ console.log(' remits-cli evidence [--limit N] [--json] # what you have run and in which world (always recorded)');
16464
+ console.log(' remits-cli verify start --summary "..." [--manifest file.json] # OPTIONAL: ask for a checkable verdict');
16465
+ console.log(' remits-cli verify [current|list|use|clear|show|status|report|claim|attach|abandon|supersede|test|token|stage|sync|tool]');
16147
16466
  console.log(' remits-cli components stage [--workset|--changed-only] [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--data-mode test|prod] [--json|--verbose]');
16148
16467
  console.log(' remits-cli components status [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--component-type TYPE --component-id ID] [--json|--verbose]');
16149
16468
  console.log(' remits-cli components lanes [--base-url URL] [--account-id ID] [--json]');
@@ -16383,6 +16702,22 @@ async function main() {
16383
16702
  throw new Error('Unknown activity subcommand: ' + subcommand);
16384
16703
  }
16385
16704
 
16705
+ if (command === 'evidence') {
16706
+ if (wantsHelp) {
16707
+ console.log('Usage: remits-cli evidence [--limit N] [--json]');
16708
+ console.log('');
16709
+ console.log('What you have actually run, and in which world. Every stage, test, token, tool and sync');
16710
+ console.log('appends one world-stamped line automatically - there is nothing to start and nothing to');
16711
+ console.log('satisfy. Grouped by world, because the same suite run in two lanes is two different facts.');
16712
+ console.log('');
16713
+ console.log('This is per-actor, so agents sharing a checkout never read each other\'s trail.');
16714
+ console.log('Use `remits-cli verify` only when someone needs a checkable VERDICT on top of this.');
16715
+ return;
16716
+ }
16717
+ await evidenceCommand(args);
16718
+ return;
16719
+ }
16720
+
16386
16721
  if (command === 'workstream') {
16387
16722
  if (wantsHelp) {
16388
16723
  console.log('Usage: remits-cli workstream status [--workstream ID|--workspace NAME] [--json]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.135",
3
+ "version": "0.1.136",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -162,11 +162,18 @@ reference named after it.
162
162
  component (preferred, because it becomes regression protection) or a browser flow through
163
163
  `remits-cli token`. "Just do it" and "that's fine, commit it" are not evidence. If you genuinely
164
164
  cannot verify, say what you would need and ask. (`development-loop.md`)
165
- - **For concrete user workflows, start a verification envelope before you edit.** `remits-cli verify
166
- start --summary "..."` records the account/source/lane tuple and a manifest when you have one.
167
- Stage, test, token, tool, and sync commands attach evidence automatically while the envelope is
168
- active; finish from `remits-cli verify report`, which separates verified claims from missing or stale
169
- evidence. (`development-loop.md`, `component-resolution.md`)
165
+ - **`remits-cli evidence` answers "what have I actually run, and in which world?"** Every stage, test,
166
+ token, tool and sync appends one world-stamped line automatically, per actor. Nothing to start,
167
+ nothing to satisfy. Read it before you re-run something, and quote it when you report what you
168
+ proved. (`development-loop.md`)
169
+ - **Verification envelopes are OPTIONAL and exist for one job: a verdict someone else will rely on.**
170
+ Start one when a ticket, a human, or a handoff needs a checkable "these specific things are true" —
171
+ not as a routine step before editing. `remits-cli verify start --summary "..."` then
172
+ `remits-cli verify claim <id> --text "..." [--test "<suite>"]` names what must be true; add
173
+ `--claim <id>` to the command that proves it; `remits-cli verify report` gives the verdict. An
174
+ envelope with no claims is simply an evidence log, which is a complete state — not something to
175
+ chase. If you find yourself opening an envelope to answer a question about your own work, use
176
+ `remits-cli evidence` instead. (`development-loop.md`, `component-resolution.md`)
170
177
  - **Read the component's `.meta.yml` before changing behavior.** Sidecar descriptions can be dated
171
178
  decision records. Before changing a displayed value, helper, calculation, schema field, or prompt
172
179
  contract, check the sidecar and either preserve its decision or explicitly supersede it.
@@ -209,8 +216,9 @@ remits-cli tools # which tools this account actually has (tools
209
216
  ```
210
217
 
211
218
  plus the repo's `account-info.json` → `resolution` block for the account's shape.
212
- For a workflow-shaped request, also run `remits-cli verify start --summary "..."` once the target tuple
213
- is understood, then keep that envelope active through stage/test/token/sync.
219
+ For a workflow-shaped request, rely on the automatic `remits-cli evidence` trail unless someone else needs
220
+ a checkable verdict. Only then start `remits-cli verify start --summary "..."`, declare claims, and keep
221
+ that envelope active through stage/test/token/sync.
214
222
 
215
223
  If any command returns 401, run `remits-cli auth` (with the same `--base-url` if you were targeting a
216
224
  non-default host).
@@ -344,10 +344,24 @@ shown is the latest run's, with the distinct worlds listed beneath it.
344
344
 
345
345
  ### Verification envelopes
346
346
 
347
- Use a verification envelope for workflow-shaped work: concrete user journeys, browser-facing changes,
348
- branch variants, subscriber/forked accounts, production-vs-test lane questions, support tickets, and
349
- multi-agent work. Start it after you know the target account/branch/workspace/data-mode tuple and before
350
- the first edit:
347
+ **First, the thing you probably want instead.** `remits-cli evidence` prints what you have actually run
348
+ and in which world, grouped by world, with no envelope and nothing to start:
349
+
350
+ ```bash
351
+ remits-cli evidence # this actor's trail
352
+ remits-cli evidence --json # structured, for your final report
353
+ ```
354
+
355
+ Every stage, test, token, tool and sync appends to it automatically. Use it for "what have I already
356
+ run?", "did that execute in the prod lane?" and "what do I put in my final message?".
357
+
358
+ **Verification envelopes are optional, and exist for a verdict someone ELSE will rely on** — a support
359
+ ticket, a human who asked you to prove specific things, a handoff another agent will act on. They are not
360
+ a routine step before editing. An envelope you started for your own benefit is nearly always
361
+ `remits-cli evidence` in disguise, and it costs you turns that prove nothing.
362
+
363
+ When a verdict is genuinely wanted, start it after you know the target
364
+ account/branch/workspace/data-mode tuple:
351
365
 
352
366
  ```bash
353
367
  remits-cli workspace use --auto
@@ -355,6 +369,35 @@ remits-cli components status
355
369
  remits-cli verify start --summary "Hosted upload updates an existing profile" --manifest acceptance.json
356
370
  ```
357
371
 
372
+ **Name what must be true, or there is no verdict to reach.** An envelope with no `claims` and no
373
+ `requiredEvidence` reports `evidence_only` — a world-stamped log. That is a complete, final state, not a
374
+ partial one, and nothing about it is outstanding. When you DO want a verdict, a manifest file is one way to
375
+ declare the contract; `verify claim` is the one-command way, and it works on a live envelope:
376
+
377
+ ```bash
378
+ remits-cli verify claim market-filter --text "US market scoring excludes AU and GB statements"
379
+ remits-cli verify claim pilot-green --text "the pilot suite passes" --test "Acquirer Pilot"
380
+ ```
381
+
382
+ Prove a claim by naming it on the command that already proves it. `--claim <id>` is stamped onto the
383
+ evidence packet every command sends, so it works on `verify test`, `token`, `tool`, `stage`, `sync` and
384
+ `attach` alike, and takes a comma-separated list:
385
+
386
+ ```bash
387
+ remits-cli verify test --test "Acquirer Pilot" --claim pilot-green
388
+ remits-cli verify tool --name mcp_run_action --as-account 36 --claim market-filter
389
+ remits-cli verify attach --claim market-filter --note "AU statements excluded and counted"
390
+ ```
391
+
392
+ A claim with no shape is satisfied by any successful packet tagged with its id. A claim that names a
393
+ `--test` suite (or `--packet-type`) must be proven by that shape. Re-declaring an id replaces it, so
394
+ tightening a claim is also one command.
395
+
396
+ **When an envelope with a contract will not close, read the report — do not start another one.** Several
397
+ open envelopes for the same goal, or workspaces named `...-r7`/`-r8`/`-r9`, is the loop signature, and
398
+ `activity inspect` reports both as smells. Close what you are not finishing: `verify supersede --envelope
399
+ <old> --superseded-by <new>` or `verify abandon --envelope <old> --reason "false start"`.
400
+
358
401
  An active envelope is stored per command world (`baseUrl + accountId + git branch + workspace`)
359
402
  inside the active local actor's mirror; **data mode is deliberately not part of the active pointer key**:
360
403
  `./.remits-cli/actors/<local-agent>/verification/active-contexts.json`. The legacy flat
@@ -367,7 +410,7 @@ history:
367
410
 
368
411
  ```bash
369
412
  remits-cli verify stage --workset
370
- remits-cli verify test --test "Adyen Import Recovery" --names "browser upload recovery"
413
+ remits-cli verify test --test "Adyen Import Recovery" --names "browser upload recovery" --claim recovery
371
414
  remits-cli verify token --path /page/pricing-config --as-account 21 --data-mode test
372
415
  remits-cli verify sync --safe
373
416
  remits-cli verify report
@@ -435,10 +478,10 @@ proof noun: `passed` for a Test case, `measured` for corpus comparisons, `synced
435
478
 
436
479
  Final claims should come from `remits-cli verify report`. Treat `Verified` as the acceptance boundary.
437
480
  `Additional evidence` is useful handoff context, but it does not satisfy a missing required packet unless
438
- the report lists it under `Verified`. If the report says `partially_verified`, stale, or missing evidence,
439
- say that plainly instead of widening the claim. In particular, staged proof is not committed variant/trunk
440
- proof, a token is not browser proof, and a direct DOM or Alpine state mutation is not the same as a user
441
- action.
481
+ the report lists it under `Verified`. If the report says `evidence_only`, no verdict was requested; if it
482
+ says `partially_verified`, stale, or missing evidence, say that plainly instead of widening the claim. In
483
+ particular, staged proof is not committed variant/trunk proof, a token is not browser proof, and a direct
484
+ DOM or Alpine state mutation is not the same as a user action.
442
485
 
443
486
  Evidence from the wrong world is excluded before it can satisfy a requirement. When the manifest declares
444
487
  fields such as `repoAccountId`, `gitBranch`, `componentBranch`, `workspace`, or `dataMode`, the report names
@@ -201,9 +201,39 @@ runs resolve and what a sync writes. If `account-info.json` carries a `component
201
201
  variants of these components exist: editing an origin component will drift them, so check
202
202
  `remits-cli components branches` before changing shared code. See `branch-variants.md`.
203
203
 
204
- If the request names a journey or acceptance behavior, start the envelope here, after the target tuple is
205
- understood and before editing. A manifest can be lightweight JSON; the point is that the required
206
- evidence is durable before the proof is collected.
204
+ **You do not need to start anything to have a record.** Every stage, test, token, tool and sync appends a
205
+ world-stamped line to your actor's evidence trail automatically. Read it with:
206
+
207
+ ```bash
208
+ remits-cli evidence
209
+ ```
210
+
211
+ That is the right tool for "what have I already run?", "did that test actually execute in the prod lane?",
212
+ and "what should I put in my final message?". It is per-actor, so agents sharing a checkout never read each
213
+ other's trail.
214
+
215
+ **Start a verification envelope only when someone else needs a verdict** — a support ticket, a human who
216
+ asked you to prove specific things, or a handoff another agent will act on. It is not a routine step before
217
+ editing, and an envelope you open for your own benefit is almost always `remits-cli evidence` in disguise.
218
+
219
+ When you do want a verdict, name what must be true. One command, no manifest file:
220
+
221
+ ```bash
222
+ remits-cli verify start --summary "Hosted upload updates an existing profile"
223
+ remits-cli verify claim fees-balance --text "statement fee totals reconcile to source within five cents"
224
+ remits-cli verify claim pilot-green --text "the pilot suite passes" --test "Acquirer Pilot"
225
+ ```
226
+
227
+ Prove a claim by naming it on the command that already proves it — `--claim <id>` works on `verify
228
+ test`, `token`, `tool`, `stage`, `sync` and `attach` alike:
229
+
230
+ ```bash
231
+ remits-cli verify test --test "Acquirer Pilot" --claim pilot-green
232
+ remits-cli verify attach --claim fees-balance --note "34025 reconciles at 0.02 variance"
233
+ ```
234
+
235
+ An envelope with no claims reports `evidence_only`. That is a complete, final state — a log, not a
236
+ half-finished exam. Nothing about it is outstanding.
207
237
 
208
238
  #### Step 2: Make the Change
209
239
  Edit component files under `components/`. This is local file editing — the platform doesn't know about your changes yet.