codeceipt 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/codeceipt.mjs +118 -22
  2. package/package.json +1 -1
package/codeceipt.mjs CHANGED
@@ -20787,12 +20787,14 @@ var itemSchema = external_exports.object({
20787
20787
  }).strict();
20788
20788
  var QUESTION_RULE = "Ask a question only when something cannot be built without guessing what the user wants (for example 'make it better'): start that line with '? ' and say what needs clarifying. A request that names what to do and where is clear (for example 'Add a comment at the top of the main page'): never ask about wording, text, style or details the agent can reasonably choose.";
20789
20789
  var ITEM_RULE = "Each item is an outcome the user would check (what is true once it is done), never a step to get there (such as finding, opening or reading a file) and never a general rule about how to write the code (such as keeping the existing code unchanged or following its style). A constraint the user asked for (for example 'passwords are never logged') is an outcome. A short request with one purpose is exactly one item, in the user's words.";
20790
+ var SPLIT_RULE = "A request that asks for several things, joined by 'and', 'then', ';' or a new sentence, is one item per thing: each quoted name to check and each change to make is its own item. Never merge several things into one item.";
20790
20791
  var DATA_RULE = "File paths, quoted text, code, package names and tool names in the request are details of a normal request: list its items as usual; never refuse and never turn the whole request into a question.";
20791
20792
  var POSTPONE_RULE = "When the user says to leave part of the request for later or to skip it for now, still list that item, starting the line with '~ ' (postponed); never drop it.";
20792
20793
  var CHECKLIST_SYSTEM = [
20793
20794
  "You split a user's software change request into a checklist. Answer only with lines, at most 10.",
20794
20795
  "Each line is one concrete, separately checkable item: a page, each distinct element, each behaviour.",
20795
20796
  ITEM_RULE,
20797
+ SPLIT_RULE,
20796
20798
  "Start a clear item with '- '.",
20797
20799
  QUESTION_RULE,
20798
20800
  POSTPONE_RULE,
@@ -21286,7 +21288,9 @@ var findingList = (value) => list(value).flatMap((raw) => {
21286
21288
  });
21287
21289
  var either = (s, key3, legacy) => s[key3] !== void 0 ? s[key3] : s[legacy];
21288
21290
  var entry = (raw) => typeof raw === "string" ? { text: raw } : obj(raw);
21289
- function receiptView(summary2) {
21291
+ var ITEM_ID = /^[\w-]{1,64}$/;
21292
+ var itemIdOf = (raw) => typeof raw === "string" && ITEM_ID.test(raw) ? raw : void 0;
21293
+ function receiptView(summary2, closedByOwner = /* @__PURE__ */ new Set()) {
21290
21294
  const s = obj(summary2);
21291
21295
  const noChanges = s.no_changes === true;
21292
21296
  const untested = new Set(
@@ -21305,11 +21309,15 @@ function receiptView(summary2) {
21305
21309
  if (text === "" || typeof status !== "string" || !STATUSES.has(status)) return [];
21306
21310
  const legacyUntested = status === "not_verified" && typeof i.id === "string" && untested.has(i.id);
21307
21311
  const shown2 = legacyUntested ? "done_untested" : noChanges && status === "missed" ? "not_done" : status;
21312
+ const itemId = itemIdOf(i.id);
21313
+ const closed = itemId !== void 0 && closedByOwner.has(itemId);
21308
21314
  return [
21309
21315
  {
21310
21316
  text,
21311
- status: shown2,
21312
- evidence: list(i.evidence).map(safePath).filter(Boolean).slice(0, 5)
21317
+ status: closed ? "done" : shown2,
21318
+ evidence: list(i.evidence).map(safePath).filter(Boolean).slice(0, 5),
21319
+ ...itemId !== void 0 ? { itemId } : {},
21320
+ ...closed ? { closedByOwner: true } : {}
21313
21321
  }
21314
21322
  ];
21315
21323
  }),
@@ -21956,6 +21964,15 @@ async function verifiedBlobs(git, root, blobs, snapshot) {
21956
21964
  }
21957
21965
  return out;
21958
21966
  }
21967
+ async function verifiedStartTexts(git, root, blobs, snapshot) {
21968
+ const out = {};
21969
+ const ids = await verifiedBlobs(git, root, blobs, snapshot);
21970
+ for (const [path3, id] of Object.entries(ids)) {
21971
+ const text = await git(["cat-file", "blob", id], root).catch(() => null);
21972
+ if (text !== null && text.length <= MAX_SCAN_BYTES) out[path3] = text;
21973
+ }
21974
+ return out;
21975
+ }
21959
21976
  async function blobOf(git, root, file2) {
21960
21977
  const abs = await inside(root, file2);
21961
21978
  const st = abs ? await lstat2(abs).catch(() => null) : null;
@@ -22238,24 +22255,36 @@ async function beforeText(git, root, base, file2) {
22238
22255
  const text = await git(["show", `${base}:./${file2}`], root).catch(() => null);
22239
22256
  return text !== null && text.length <= MAX_SCAN_BYTES ? text : null;
22240
22257
  }
22241
- async function localIntegrity(git, root, base, files) {
22258
+ async function localIntegrity(git, root, base, files, startTexts = {}) {
22242
22259
  const relevant = files.filter((f) => isTestFile(f) || isTestConfig(f)).slice(0, MAX_FILES2);
22243
22260
  const changes = [];
22261
+ const earlier = [];
22244
22262
  for (const path3 of relevant) {
22245
22263
  const [before, after] = await Promise.all([
22246
22264
  base ? beforeText(git, root, base, path3) : Promise.resolve(null),
22247
22265
  afterText(root, path3)
22248
22266
  ]);
22267
+ const start = Object.hasOwn(startTexts, path3) ? startTexts[path3] : void 0;
22268
+ if (start !== void 0) {
22269
+ changes.push({ path: path3, before: start, after });
22270
+ earlier.push({ path: path3, before, after: start });
22271
+ continue;
22272
+ }
22249
22273
  if (before === null && after === null) continue;
22250
22274
  changes.push({ path: path3, before, after });
22251
22275
  }
22252
- const flags = checkTestIntegrity(changes).sort((a, b) => PRIORITY.indexOf(a.kind) - PRIORITY.indexOf(b.kind)).map((f) => ({
22276
+ const clean3 = (list2) => list2.sort((a, b) => PRIORITY.indexOf(a.kind) - PRIORITY.indexOf(b.kind)).map((f) => ({
22253
22277
  ...f,
22254
22278
  file: safePath(f.file),
22255
22279
  names: f.names.map((n) => safeDisplay(n, 200)).filter(Boolean),
22256
22280
  count: Math.min(f.count, MAX_COUNT2)
22257
22281
  }));
22258
- return { flags: flags.slice(0, MAX_FLAGS), omitted: Math.max(0, flags.length - MAX_FLAGS) };
22282
+ const flags = clean3(checkTestIntegrity(changes));
22283
+ return {
22284
+ flags: flags.slice(0, MAX_FLAGS),
22285
+ omitted: Math.max(0, flags.length - MAX_FLAGS),
22286
+ beforeSession: clean3(checkTestIntegrity(earlier))
22287
+ };
22259
22288
  }
22260
22289
  async function passingTests(root, reportPath) {
22261
22290
  if (!reportPath) return null;
@@ -22366,6 +22395,13 @@ var head = (git, root) => git(["rev-parse", "--verify", "-q", "HEAD"], root).the
22366
22395
  var count = (v) => Array.isArray(v) ? v.length : 0;
22367
22396
  var number4 = (v) => typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? Math.min(v, 1e6) : 0;
22368
22397
  var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
22398
+ var PRE_TEST_LABEL = {
22399
+ skipped_tests: "skipped test",
22400
+ deleted_tests: "deleted test",
22401
+ deleted_test_file: "deleted test file",
22402
+ removed_assertions: "removed assertion",
22403
+ test_config_changed: "changed test config"
22404
+ };
22369
22405
  function receiptSummary(receipt, config2, receiptId, filesChanged) {
22370
22406
  const grouped = Array.isArray(receipt.files_by_feature) ? receipt.files_by_feature.reduce(
22371
22407
  (n, g) => n + count(typeof g === "object" && g !== null ? g.files : null),
@@ -22380,7 +22416,14 @@ function receiptSummary(receipt, config2, receiptId, filesChanged) {
22380
22416
  const untested = status("done_untested");
22381
22417
  const done = status("done") + untested;
22382
22418
  const partial2 = status("partial");
22383
- const missed = status("missed") + partial2;
22419
+ const missed = status("missed");
22420
+ const notVerified = status("not_verified");
22421
+ const states = [
22422
+ done > 0 ? `${done} done` : "",
22423
+ partial2 > 0 ? `${partial2} partly done` : "",
22424
+ missed > 0 ? `${missed} missed` : "",
22425
+ notVerified > 0 ? `${notVerified} not verified` : ""
22426
+ ].filter(Boolean);
22384
22427
  const postponed = status("postponed");
22385
22428
  const present = status("already_present");
22386
22429
  const questions = status("question");
@@ -22416,7 +22459,7 @@ function receiptSummary(receipt, config2, receiptId, filesChanged) {
22416
22459
  receipt.no_request === true ? `Codeceipt receipt: ${changed ? `${changed}. ` : ""}${NO_REQUEST_NOTE}` : receipt.no_changes === true ? (
22417
22460
  // Real-test F: a request but no changed file still gets a receipt.
22418
22461
  `Codeceipt receipt: No files changed; ${status("missed")} not done${present > 0 ? `, ${present} already present` : ""}.`
22419
- ) : `Codeceipt receipt: ${changed ? `${changed}; ` : ""}${done} done, ${missed} missed${partial2 > 0 ? ` (${partial2} partly done)` : ""}.`,
22462
+ ) : `Codeceipt receipt: ${changed ? `${changed}; ` : ""}${states.length > 0 ? states.join(", ") : "no items"}.`,
22420
22463
  `Checks needing attention: ${attention.length > 0 ? attention.join(", ") : "none"}.`,
22421
22464
  ...extra.length > 0 ? [`Also listed: ${extra.join(", ")}.`] : []
22422
22465
  ];
@@ -22591,11 +22634,18 @@ async function sessionEnd(_agent, deps, opts = {}) {
22591
22634
  const session = new Set(files);
22592
22635
  const sessionFindings = scan?.findings.filter((f) => session.has(f.file)) ?? [];
22593
22636
  const beforeFindings = scan?.findings.filter((f) => !session.has(f.file)) ?? [];
22637
+ const startTexts = snapshot ? await verifiedStartTexts(
22638
+ deps.git,
22639
+ root,
22640
+ Object.fromEntries(Object.entries(blobs).filter(([path3]) => preExisting.includes(path3))),
22641
+ snapshot
22642
+ ).catch(() => ({})) : {};
22594
22643
  const integrity = await localIntegrity(
22595
22644
  deps.git,
22596
22645
  root,
22597
22646
  start ?? (commit ? "HEAD" : null),
22598
- files
22647
+ files,
22648
+ startTexts
22599
22649
  ).catch(() => null);
22600
22650
  const passingAfter = await passingTests(root, brandEnv(deps.env ?? {}, "TEST_REPORT"));
22601
22651
  const summaries = await changeSummaries(
@@ -22748,6 +22798,13 @@ async function sessionEnd(_agent, deps, opts = {}) {
22748
22798
  `${WARN} ${testWarnings} sign(s) that tests were removed or weakened; see the receipt.`
22749
22799
  );
22750
22800
  }
22801
+ const earlier = integrity?.beforeSession ?? [];
22802
+ if (earlier.length > 0) {
22803
+ const items = earlier.slice(0, MAX_WARNED_ITEMS).map((f) => `${plural2(f.count, PRE_TEST_LABEL[f.kind] ?? "test change")} in ${f.file}`);
22804
+ warnings.push(
22805
+ `${WARN} already modified before this session, not counted: ${items.join("; ")}.`
22806
+ );
22807
+ }
22751
22808
  if (scan && scan.findings.length + scan.omitted > 0) {
22752
22809
  warnings.push(
22753
22810
  `${WARN} ${scan.findings.length + scan.omitted} possible secret(s) in the changed files; see the receipt.`
@@ -23809,7 +23866,7 @@ var STOP_HOOK = hook(BRAND, "stop");
23809
23866
  var LEGACY_START_HOOK = hook(LEGACY, "session-start");
23810
23867
  var LEGACY_END_HOOK = hook(LEGACY, "session-end");
23811
23868
  var NODE_PATHS = '"$PATH:/usr/local/bin:/opt/homebrew/bin:$HOME/.volta/bin:$HOME/.npm-global/bin:$HOME/.local/bin"';
23812
- var CLI_VERSION = "0.1.1";
23869
+ var CLI_VERSION = "0.1.2";
23813
23870
  var wrap = (args, version2) => `PATH=${NODE_PATHS}; if command -v codeceipt >/dev/null 2>&1; then codeceipt ${args}; else npx -y codeceipt@${version2} ${args}; fi`;
23814
23871
  var shellHook = (bare) => wrap(bare.replace(/^codeceipt /, ""), CLI_VERSION);
23815
23872
  var isWrapped = (command, bare) => {
@@ -24728,6 +24785,46 @@ async function runMcpProxy(lines, config2, fetchImpl, write2) {
24728
24785
  }
24729
24786
  }
24730
24787
 
24788
+ // src/owner-states.ts
24789
+ var PLAIN_ID = /^[\w-]{1,64}$/;
24790
+ var MAX_IDS = 500;
24791
+ function plainIds(value) {
24792
+ const out = /* @__PURE__ */ new Set();
24793
+ if (!Array.isArray(value)) return out;
24794
+ for (const id of value.slice(0, MAX_IDS)) {
24795
+ if (typeof id === "string" && PLAIN_ID.test(id)) out.add(id);
24796
+ }
24797
+ return out;
24798
+ }
24799
+ function applyOwnerStates(receipt, closedIds, dismissedIds) {
24800
+ if (!Array.isArray(receipt.items)) return receipt;
24801
+ const closed = plainIds(closedIds);
24802
+ const dismissed = plainIds(dismissedIds);
24803
+ if (closed.size === 0 && dismissed.size === 0) return receipt;
24804
+ const items = receipt.items.map((item) => {
24805
+ if (typeof item !== "object" || item === null) return item;
24806
+ const id = item.id;
24807
+ if (typeof id !== "string") return item;
24808
+ if (dismissed.has(id)) return { ...item, status: "dismissed" };
24809
+ if (closed.has(id)) return { ...item, status: "closed_by_owner" };
24810
+ return item;
24811
+ });
24812
+ return { ...receipt, items };
24813
+ }
24814
+ function ownerStateLines(receipt) {
24815
+ if (!Array.isArray(receipt.items)) return [];
24816
+ const lines = [];
24817
+ for (const item of receipt.items) {
24818
+ if (typeof item !== "object" || item === null) continue;
24819
+ const { status, text } = item;
24820
+ const shown2 = typeof text === "string" ? safeDisplay(text, 200) : "";
24821
+ if (shown2 === "") continue;
24822
+ if (status === "closed_by_owner") lines.push(`Closed by you: ${shown2}`);
24823
+ if (status === "dismissed") lines.push(`Dismissed: ${shown2}`);
24824
+ }
24825
+ return lines.slice(0, 30);
24826
+ }
24827
+
24731
24828
  // src/repo-link.ts
24732
24829
  import {
24733
24830
  constants as constants3,
@@ -25426,25 +25523,24 @@ async function runLast(deps, path3, evidence = false) {
25426
25523
  "Saved server address must be https: run `npx codeceipt` again."
25427
25524
  );
25428
25525
  }
25429
- const answer = await request(
25430
- deps.fetch,
25431
- config2,
25432
- "GET",
25433
- `/api/hooks/${encodeURIComponent(config2.projectId)}/last-receipt`
25434
- );
25435
- const receipt = answer.receipt;
25436
- if (typeof receipt !== "object" || receipt === null) {
25526
+ const answer = await request(deps.fetch, config2, "GET", `/api/hooks/${encodeURIComponent(config2.projectId)}/last-receipt`);
25527
+ const stored = answer.receipt;
25528
+ if (typeof stored !== "object" || stored === null) {
25437
25529
  deps.stdout("No receipt yet: make a change with your agent and end the session.");
25438
25530
  return EXIT.ok;
25439
25531
  }
25532
+ const receipt = applyOwnerStates(
25533
+ stored,
25534
+ answer.owner_closed_ids,
25535
+ answer.dismissed_ids
25536
+ );
25440
25537
  const id = typeof answer.receipt_id === "string" && /^[\w-]{1,64}$/.test(answer.receipt_id) ? answer.receipt_id : null;
25441
25538
  const at = typeof answer.created_at === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(answer.created_at) ? ` (${answer.created_at.slice(0, 16).replace("T", " ")} UTC)` : "";
25442
25539
  deps.stdout(`Latest receipt${at}:`);
25443
- for (const line2 of receiptSummary(receipt, config2, id))
25444
- deps.stdout(line2);
25540
+ for (const line2 of receiptSummary(receipt, config2, id)) deps.stdout(line2);
25541
+ for (const line2 of ownerStateLines(receipt)) deps.stdout(line2);
25445
25542
  for (const line2 of attentionLines(receipt)) deps.stdout(line2);
25446
- if (evidence)
25447
- for (const line2 of evidenceLines(receipt)) deps.stdout(line2);
25543
+ if (evidence) for (const line2 of evidenceLines(receipt)) deps.stdout(line2);
25448
25544
  return EXIT.ok;
25449
25545
  }
25450
25546
  var OWN_COMMAND = /^!?\s*(npx\s+)?codeceipt\b/i;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceipt",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A receipt for every AI change.",
5
5
  "keywords": [
6
6
  "claude-code",