vouchington-tooling 0.44.0 → 0.46.0

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 (124) hide show
  1. package/README.md +10 -52
  2. package/dist/agent-blackboard/client.d.mts +6 -0
  3. package/dist/agent-blackboard/feedback-audit-source.d.mts +4 -10
  4. package/dist/agent-blackboard/feedback-audit-source.mjs +7 -18
  5. package/dist/agent-blackboard/feedback-compose-audit.mjs +6 -14
  6. package/dist/agent-blackboard/feedback-compose.d.mts +1 -4
  7. package/dist/agent-blackboard/feedback-compose.mjs +2 -5
  8. package/dist/agent-blackboard/feedback-fields.mjs +1 -1
  9. package/dist/agent-blackboard/feedback-identity.mjs +1 -1
  10. package/dist/agent-blackboard/feedback-journal-audit.d.mts +6 -8
  11. package/dist/agent-blackboard/feedback-journal-audit.mjs +9 -10
  12. package/dist/agent-blackboard/feedback-journal-sandbox.d.mts +2 -2
  13. package/dist/agent-blackboard/feedback-journal-sandbox.mjs +2 -2
  14. package/dist/agent-blackboard/feedback-outbox-rejected.mjs +1 -1
  15. package/dist/agent-blackboard/feedback-outbox.mjs +2 -2
  16. package/dist/agent-blackboard/index.d.mts +1 -1
  17. package/dist/audit-journal/ci-failures.d.mts +6 -0
  18. package/dist/{session-friction → audit-journal}/ci-failures.mjs +3 -9
  19. package/dist/{session-friction → audit-journal}/journal.d.mts +0 -1
  20. package/dist/{session-friction → audit-journal}/journal.mjs +1 -1
  21. package/dist/audit-journal/session-id.d.mts +1 -0
  22. package/dist/{session-friction → audit-journal}/session-id.mjs +0 -5
  23. package/dist/{session-friction → audit-journal}/text.d.mts +1 -4
  24. package/dist/{session-friction → audit-journal}/text.mjs +4 -22
  25. package/dist/audit-journal/types.d.mts +17 -0
  26. package/dist/cli/commands/gha-runtime-audit.mjs +2 -0
  27. package/dist/cli/usage.d.mts +1 -1
  28. package/dist/cli/usage.mjs +1 -1
  29. package/dist/gh-cli/env.d.mts +9 -0
  30. package/dist/gh-cli/env.mjs +12 -0
  31. package/dist/gh-cli/exec.d.mts +7 -4
  32. package/dist/gh-cli/exec.mjs +7 -5
  33. package/dist/gh-cli/index.d.mts +1 -0
  34. package/dist/gh-cli/index.mjs +1 -0
  35. package/dist/gha-post-review/github.mjs +2 -1
  36. package/dist/index.d.mts +3 -3
  37. package/dist/index.mjs +2 -2
  38. package/dist/mcp-server/context.d.mts +5 -0
  39. package/dist/mcp-server/dispatch.d.mts +5 -0
  40. package/dist/mcp-server/dispatch.mjs +8 -1
  41. package/dist/mcp-server/fake-blackboard.test-helpers.d.mts +2 -0
  42. package/dist/mcp-server/fake-blackboard.test-helpers.mjs +3 -0
  43. package/dist/mcp-server/handlers-pr.d.mts +1 -1
  44. package/dist/mcp-server/handlers-pr.mjs +6 -97
  45. package/dist/mcp-server/handlers-retrospective-save.d.mts +10 -0
  46. package/dist/mcp-server/handlers-retrospective-save.mjs +138 -0
  47. package/dist/mcp-server/handlers-retrospective.d.mts +1 -12
  48. package/dist/mcp-server/handlers-retrospective.mjs +1 -154
  49. package/dist/mcp-server/handlers-session.mjs +25 -6
  50. package/dist/mcp-server/handlers-snapshot-cleanup.d.mts +7 -0
  51. package/dist/mcp-server/handlers-snapshot-cleanup.mjs +36 -0
  52. package/dist/mcp-server/harness.test-helpers.d.mts +3 -0
  53. package/dist/mcp-server/harness.test-helpers.mjs +2 -0
  54. package/dist/mcp-server/policy-run.d.mts +1 -0
  55. package/dist/mcp-server/policy-run.mjs +1 -0
  56. package/dist/mcp-server/pr-agent.d.mts +12 -0
  57. package/dist/mcp-server/pr-agent.mjs +34 -0
  58. package/dist/mcp-server/pr-body.d.mts +6 -2
  59. package/dist/mcp-server/pr-body.mjs +16 -5
  60. package/dist/mcp-server/pr-defaults.d.mts +3 -0
  61. package/dist/mcp-server/pr-defaults.mjs +7 -2
  62. package/dist/mcp-server/pr-gh.mjs +2 -1
  63. package/dist/mcp-server/pr-input.d.mts +14 -6
  64. package/dist/mcp-server/pr-input.mjs +22 -5
  65. package/dist/mcp-server/pr-live.d.mts +4 -1
  66. package/dist/mcp-server/pr-live.mjs +8 -3
  67. package/dist/mcp-server/pr-mode.d.mts +7 -0
  68. package/dist/mcp-server/pr-mode.mjs +34 -0
  69. package/dist/mcp-server/pr-no-source.d.mts +9 -0
  70. package/dist/mcp-server/pr-no-source.mjs +22 -0
  71. package/dist/mcp-server/pr-prepare.d.mts +25 -0
  72. package/dist/mcp-server/pr-prepare.mjs +124 -0
  73. package/dist/mcp-server/preview-registry.d.mts +25 -0
  74. package/dist/mcp-server/preview-registry.mjs +38 -0
  75. package/dist/mcp-server/retrospective-check.d.mts +3 -0
  76. package/dist/mcp-server/retrospective-check.mjs +38 -0
  77. package/dist/mcp-server/retrospective-input.mjs +2 -2
  78. package/dist/mcp-server/retrospective-preview.d.mts +17 -0
  79. package/dist/mcp-server/retrospective-preview.mjs +60 -0
  80. package/dist/mcp-server/server.mjs +1 -0
  81. package/dist/mcp-server/snapshot-registry.d.mts +25 -0
  82. package/dist/mcp-server/snapshot-registry.mjs +32 -0
  83. package/dist/mcp-server/tools-pr.mjs +28 -16
  84. package/dist/mcp-server/tools-retrospective.mjs +25 -5
  85. package/dist/mcp-server/tools.mjs +6 -0
  86. package/dist/{session-friction → private-files}/lock.mjs +3 -2
  87. package/dist/private-files/log-file.d.mts +3 -0
  88. package/dist/private-files/log-file.mjs +53 -0
  89. package/dist/{session-friction → private-files}/utf8.mjs +1 -1
  90. package/dist/retrospective-facts/exec.mjs +2 -1
  91. package/docs/mcp-server.md +112 -26
  92. package/package.json +3 -8
  93. package/skills/retrospective/SKILL.md +3 -1
  94. package/skills/retrospective-distill/SKILL.md +5 -0
  95. package/dist/session-friction/ci-failures.d.mts +0 -13
  96. package/dist/session-friction/classify.d.mts +0 -2
  97. package/dist/session-friction/classify.mjs +0 -98
  98. package/dist/session-friction/command-segments.d.mts +0 -3
  99. package/dist/session-friction/command-segments.mjs +0 -94
  100. package/dist/session-friction/coverage-ledger.d.mts +0 -10
  101. package/dist/session-friction/coverage-ledger.mjs +0 -75
  102. package/dist/session-friction/directory.d.mts +0 -1
  103. package/dist/session-friction/directory.mjs +0 -71
  104. package/dist/session-friction/index.d.mts +0 -6
  105. package/dist/session-friction/index.mjs +0 -5
  106. package/dist/session-friction/log-file.d.mts +0 -7
  107. package/dist/session-friction/log-file.mjs +0 -95
  108. package/dist/session-friction/log.d.mts +0 -8
  109. package/dist/session-friction/log.mjs +0 -163
  110. package/dist/session-friction/normalize.d.mts +0 -1
  111. package/dist/session-friction/normalize.mjs +0 -81
  112. package/dist/session-friction/report.d.mts +0 -16
  113. package/dist/session-friction/report.mjs +0 -91
  114. package/dist/session-friction/sandbox.d.mts +0 -3
  115. package/dist/session-friction/sandbox.mjs +0 -43
  116. package/dist/session-friction/session-id.d.mts +0 -2
  117. package/dist/session-friction/types.d.mts +0 -80
  118. /package/dist/{session-friction → audit-journal}/audit-block.d.mts +0 -0
  119. /package/dist/{session-friction → audit-journal}/audit-block.mjs +0 -0
  120. /package/dist/{session-friction → audit-journal}/audit-fit.d.mts +0 -0
  121. /package/dist/{session-friction → audit-journal}/audit-fit.mjs +0 -0
  122. /package/dist/{session-friction → audit-journal}/types.mjs +0 -0
  123. /package/dist/{session-friction → private-files}/lock.d.mts +0 -0
  124. /package/dist/{session-friction → private-files}/utf8.d.mts +0 -0
package/README.md CHANGED
@@ -315,7 +315,6 @@ import {
315
315
  } from 'vouchington-tooling/vitest-diagnostics'
316
316
  import { runRetrospectiveTranscript } from 'vouchington-tooling/retrospective-transcript'
317
317
  import { appendJournal, probeBlackboard } from 'vouchington-tooling/agent-blackboard'
318
- import { buildSessionFrictionReport, recordFriction } from 'vouchington-tooling/session-friction'
319
318
  import { GITHUB_BODY_MAX_CHARACTERS, validateGitHubBodyLength } from 'vouchington-tooling'
320
319
  import { createPullRequest, processDiffCommand, runGh, runGit } from 'vouchington-tooling/gh-cli'
321
320
  import {
@@ -475,41 +474,6 @@ audit finishes, when the process receives SIGINT, SIGTERM, or SIGHUP, and on the
475
474
  previous process died first, including SIGKILL. Each workspace records its owner's PID so a later
476
475
  audit can delete directories whose owner is gone.
477
476
 
478
- `session-friction` is an opt-in capture and reporting library. Callers supply the session id,
479
- absolute log directory, host-independent observation, and journal loader; it does not inspect host
480
- environment variables, install hooks, or connect to a journal service by itself. Invoking
481
- `recordFriction` touches the session log even when no event is classified, preserving the
482
- difference between an observed clean session and missing evidence. Report markdown keeps backend
483
- diagnostics separate from its paste-safe output. Capture stores at most 500 events per session and counts rejected captures in a fixed-size atomic
484
- sidecar under the same session lock for either the event or byte limit, with visible `truncated`/`droppedCount` metadata. Saturated
485
- capture reads only the bounded counter; reports also preserve historical overflow markers.
486
- Interrupted or inconsistent counter updates make coverage unavailable. Malformed records also make the scan partial. It
487
- truncates event detail to 1,000 characters, and consumes up to 500 entries from the journal loader
488
- when building a report, stopping earlier when its aggregate 1 MB inspected-byte budget is reached.
489
- Bounded journal scans that stop before exhaustion are reported as incomplete rather than clean.
490
- Report liveness inherits the caller-supplied journal loader, which must bound its own I/O and yields.
491
- Log reads are capped at 2 MB, journal Markdown at 10,000 bytes per entry,
492
- and rendered audit fields are kept whole: composition loads the journal and friction log once, redacts each field from its raw text before Markdown escaping (a caller-supplied redactor runs first, then the built-in one), then renders the CI and sandbox sections, measures the real envelope against its 12,000-byte markdown and 16,384-byte envelope limits, and shrinks them in a few bounded attempts. A block shrinks Root diagnostic first and Evidence last, cutting only at token boundaries (never inside a URL or SHA) with a trailing `…`; groups that still cannot fit are omitted and counted in an explicit conforming group that points at the journal. The final attempt keeps only that omission note (about 300 bytes), so an input within roughly 300 bytes of the limit can still fail to compose. A caller-supplied `budgets` cap limits rendered groups only; the omission note is exempt from it. Permission requests record requested outcomes; explicit result observations may record approved
493
- or denied outcomes. A decision and simultaneous tool failure are both retained and counted in
494
- the report. Escalation detail without a decision remains unknown. Localhost connection
495
- refusal is an ambiguous failure and does not establish a sandbox cause. The supplied log directory must be dedicated
496
- to session-friction; existing directories must already be owner-only, while newly created
497
- directories and log files are enforced as owner-only when recording. Reads use a fixed bounded
498
- buffer that can detect growth one byte beyond the documented 2 MB cap.
499
- Ownership checks require POSIX effective-user IDs (Linux and macOS); session-friction throws on
500
- Windows and other platforms where those IDs are unavailable.
501
- Root-owned system symlink ancestors are supported for paths such as macOS `/var`; callers must not
502
- allow the directory chain to be mutated while it is being validated.
503
- Command-prefix normalization recognizes simple shell
504
- segments with single or double quotes; it does not evaluate substitutions or implement a full shell
505
- grammar. Normalization attempts limited redaction of obvious credential patterns but is not a secret
506
- scrubber; callers must ensure credentials are never included in captured commands.
507
- Failure classification inspects at most 100,000 structured-stderr characters, split evenly between
508
- the beginning and end when input exceeds that bound.
509
- Cooperating log readers and writers are serialized, including the initial clean-session
510
- touch. Recording and report log reads are synchronous: on contention they block the caller's
511
- event loop for up to one second before failing explicitly. Avoid these APIs on hot request paths.
512
-
513
477
  The artifact, review-payload, HTTP body, and pagination APIs validate untrusted inputs at their
514
478
  boundaries. Neutral review posting lives in `gha-post-review` and requires a caller-supplied GitHub
515
479
  token. Claude App OIDC is an explicitly selected `gha-claude-post-review` adapter; the legacy
@@ -669,31 +633,25 @@ replay the identical envelope and source ID to verify its actual persistence.
669
633
  generates required sections and markers. Tool and architectural assessments preserve observation,
670
634
  evidence, disposition and tracking reference. None-observed and unassessed states need a scoped
671
635
  reason. Complete coverage is rejected when required sources are unavailable or capture is partial.
672
- Typed collector reports expose availability independently of rendered text. Composition rejects
673
- aggregate dropped counts below observed friction drops and retains larger caller aggregates without
674
- double counting. Generated frontmatter carries validated `work_outcome` and `feedback_coverage`
636
+ Typed collector reports expose availability independently of rendered text. Generated frontmatter carries validated `work_outcome` and `feedback_coverage`
675
637
  for consumers to preserve or reject conflicting explicit overrides; trusted mode stays external.
676
638
  Human top-five summaries do not authorize dropping additional durable findings.
677
639
 
678
640
  The `## CI Failures` and `## Sandbox & Permission Audit` sections come from one optional audit
679
- source; the two inputs are mutually exclusive and supplying both throws before any collector runs.
680
- `friction: {directory, journalLoader, ...}` keeps reading the session-friction log together with the
681
- journal and is unchanged. `journal: {journalLoader}` needs no friction directory or log: both
682
- sections are assessed from the session's journal entries alone, and the report carries
683
- `coverage.frictionStatus: 'journal-only'` (in place of `empty` or `events`) so the text states that
684
- no log was observed. `feedbackCoverage.status: 'complete'` accepts either source. It is rejected
685
- when neither is supplied (the error names the missing source), and a journal-only audit does not
686
- count as assessed when the loader reports `not-found` (there is no log to establish that the session
687
- was observed), throws, or is truncated by the scan bounds.
641
+ source, `journal: {journalLoader}`: both sections are assessed from the session's journal entries
642
+ alone, with the text stating `journal entries only`. `feedbackCoverage.status: 'complete'` is
643
+ rejected when no journal source is supplied (the error names the missing source), and a journal
644
+ audit does not count as assessed when the loader reports `not-found` (nothing else establishes that
645
+ the session was observed), throws, or is truncated by the scan bounds.
688
646
 
689
647
  ```ts
690
648
  await composeRetrospective({ ...input, journal: { journalLoader } })
691
649
  ```
692
650
 
693
- `journalLoader` is the session-friction `JournalLoader`; `JournalLoader`, `JournalLoadResult`,
651
+ `journalLoader` is a `JournalLoader`; `JournalLoader`, `JournalLoadResult`,
694
652
  `JournalEntry`, and `JournalAuditOptions` are exported as types from
695
- `vouchington-tooling/agent-blackboard`. The `## CI Failures` block grammar and its three status
696
- lines (`failures observed`, `none observed`, `unavailable (<reason>)`) are the same for both sources.
653
+ `vouchington-tooling/agent-blackboard`. The `## CI Failures` block grammar has three status
654
+ lines (`failures observed`, `none observed`, `unavailable (<reason>)`).
697
655
  Journal-only sandbox and permission entries are journal Markdown entries that hold exactly one
698
656
  block, escaped and length-bounded like CI failures, and other entries are ignored:
699
657
 
@@ -709,7 +667,7 @@ block, escaped and length-bounded like CI failures, and other entries are ignore
709
667
 
710
668
  `ambiguous-failure` takes the same shape as `sandbox-failure`. The section reports
711
669
  `Status: none observed` or `Status: events observed`, each followed by
712
- `(journal entries only; no friction log observed)`; a truncated scan reports
670
+ `(journal entries only)`; a truncated scan reports
713
671
  `unavailable (journal scan incomplete)` with the entries read so far.
714
672
 
715
673
  ## Basic Auth exemption facts
@@ -32,6 +32,12 @@ export type BlackboardClientModule = {
32
32
  Snapshots?: new (connection: BlackboardConnection) => {
33
33
  export(options?: unknown): Promise<unknown>;
34
34
  };
35
+ /** Newer clients own the ownership markers of their exports and clean them with a capability. */
36
+ cleanupSnapshotPartitions?: (options: {
37
+ path?: string;
38
+ directory?: string;
39
+ cleanupToken?: string;
40
+ }) => Promise<void>;
35
41
  };
36
42
  type BlackboardClientLoader = () => Promise<BlackboardClientModule>;
37
43
  export type BlackboardClientDependencies = {
@@ -1,17 +1,11 @@
1
- import { type FrictionReportInputs, type SessionFrictionReport, type SessionFrictionReportOptions } from '../session-friction/index.mts';
2
- import type { JournalScan } from '../session-friction/journal.mts';
1
+ import type { JournalScan } from '../audit-journal/journal.mts';
3
2
  import { type JournalAuditOptions, type JournalAuditReport } from './feedback-journal-audit.mts';
4
3
  export type AuditSourceInput = {
5
- friction?: SessionFrictionReportOptions;
6
4
  journal?: JournalAuditOptions;
7
5
  };
8
- export type AuditReport = SessionFrictionReport | JournalAuditReport;
9
- export declare function assertSingleAuditSource(input: AuditSourceInput): void;
6
+ export type AuditReport = JournalAuditReport;
10
7
  /** What the audit sections read from outside, loaded once and rendered as often as needed. */
11
8
  export type AuditInputs = {
12
- kind: 'friction';
13
- loaded: FrictionReportInputs;
14
- } | {
15
9
  kind: 'journal';
16
10
  scanned: JournalScan;
17
11
  } | {
@@ -19,7 +13,7 @@ export type AuditInputs = {
19
13
  };
20
14
  export declare function loadAuditInputs(sessionId: string, input: AuditSourceInput): Promise<AuditInputs>;
21
15
  /** Builds the CI-failure and sandbox sections from loaded inputs, with no further I/O. */
22
- export declare function renderAuditReport(sessionId: string, input: AuditSourceInput, inputs: AuditInputs): AuditReport;
23
- /** Only a real log status or an explicit journal-only scan counts as assessed. */
16
+ export declare function renderAuditReport(input: AuditSourceInput, inputs: AuditInputs): AuditReport;
17
+ /** Only a complete, untruncated journal scan counts as assessed. */
24
18
  export declare function auditAssessed({ coverage }: AuditReport): boolean;
25
19
  export declare function completeCoverageError(input: AuditSourceInput): Error;
@@ -1,37 +1,26 @@
1
- import { loadFrictionReportInputs, renderFrictionReport, } from "../session-friction/index.mjs";
2
1
  import { loadJournalAuditInput, renderJournalAuditReport, } from "./feedback-journal-audit.mjs";
3
2
  const ASSESSED_SOURCE_ERROR = 'complete feedback coverage requires assessed available factual and finding sources';
4
3
  const NOT_ASSESSED_MARKDOWN = '## CI Failures\nStatus: unavailable (not assessed)\n\n## Sandbox & Permission Audit\nStatus: unavailable (not assessed)';
5
- export function assertSingleAuditSource(input) {
6
- if (input.friction && input.journal)
7
- throw new Error('composition accepts either a friction or a journal audit source, not both');
8
- }
9
4
  export async function loadAuditInputs(sessionId, input) {
10
- if (input.friction)
11
- return { kind: 'friction', loaded: await loadFrictionReportInputs(sessionId, input.friction) };
12
5
  if (input.journal)
13
6
  return { kind: 'journal', scanned: await loadJournalAuditInput(sessionId, input.journal) };
14
7
  return { kind: 'none' };
15
8
  }
16
9
  /** Builds the CI-failure and sandbox sections from loaded inputs, with no further I/O. */
17
- export function renderAuditReport(sessionId, input, inputs) {
18
- if (inputs.kind === 'friction')
19
- return renderFrictionReport(sessionId, input.friction, inputs.loaded);
10
+ export function renderAuditReport(input, inputs) {
20
11
  if (inputs.kind === 'journal')
21
- return renderJournalAuditReport(sessionId, input.journal, inputs.scanned);
12
+ return renderJournalAuditReport(input.journal, inputs.scanned);
22
13
  return {
23
- coverage: { journalStatus: 'unavailable', frictionStatus: 'absent', truncated: false },
14
+ coverage: { journalStatus: 'unavailable', truncated: false },
24
15
  markdown: NOT_ASSESSED_MARKDOWN,
25
16
  };
26
17
  }
27
- /** Only a real log status or an explicit journal-only scan counts as assessed. */
18
+ /** Only a complete, untruncated journal scan counts as assessed. */
28
19
  export function auditAssessed({ coverage }) {
29
- return (coverage.journalStatus === 'complete' &&
30
- ['empty', 'events', 'journal-only'].includes(coverage.frictionStatus) &&
31
- !coverage.truncated);
20
+ return coverage.journalStatus === 'complete' && !coverage.truncated;
32
21
  }
33
22
  export function completeCoverageError(input) {
34
- return new Error(input.friction || input.journal
23
+ return new Error(input.journal
35
24
  ? ASSESSED_SOURCE_ERROR
36
- : `${ASSESSED_SOURCE_ERROR} (no friction or journal source supplied for CI failures and sandbox audit)`);
25
+ : `${ASSESSED_SOURCE_ERROR} (no journal source supplied for CI failures and sandbox audit)`);
37
26
  }
@@ -25,21 +25,13 @@ export function boundedAuditSource(input, caps) {
25
25
  ciBytes: Math.min(caller?.ciBytes ?? Infinity, caps.ciBytes),
26
26
  sandboxBytes: Math.min(caller?.sandboxBytes ?? Infinity, caps.sandboxBytes),
27
27
  });
28
- return input.friction
28
+ return input.journal
29
29
  ? {
30
- friction: {
31
- ...input.friction,
32
- redact: redactionFor(input.friction.redact),
33
- budgets: clamp(input.friction.budgets),
30
+ journal: {
31
+ ...input.journal,
32
+ redact: redactionFor(input.journal.redact),
33
+ budgets: clamp(input.journal.budgets),
34
34
  },
35
35
  }
36
- : input.journal
37
- ? {
38
- journal: {
39
- ...input.journal,
40
- redact: redactionFor(input.journal.redact),
41
- budgets: clamp(input.journal.budgets),
42
- },
43
- }
44
- : {};
36
+ : {};
45
37
  }
@@ -1,6 +1,5 @@
1
1
  import { type RetrospectiveFactsOptions } from '../retrospective-facts/index.mts';
2
2
  import { type ResolveOptions } from '../retrospective-transcript/index.mts';
3
- import type { SessionFrictionReportOptions } from '../session-friction/index.mts';
4
3
  import type { JournalAuditOptions } from './feedback-journal-audit.mts';
5
4
  import type { FeedbackCoverage, FeedbackReference, WorkOutcome } from './feedback-types.mts';
6
5
  export type FeedbackAssessment = {
@@ -29,9 +28,7 @@ export type RetrospectiveCompositionInput = {
29
28
  narrative: string;
30
29
  facts: Omit<RetrospectiveFactsOptions, 'raw'> | Unassessed;
31
30
  transcript: ResolveOptions | Unassessed;
32
- /** CI failures and sandbox audit from a friction log plus journal. Exclusive with `journal`. */
33
- friction?: SessionFrictionReportOptions;
34
- /** CI failures and sandbox audit from journal entries alone (`frictionStatus: 'journal-only'`). */
31
+ /** CI failures and sandbox audit from the session journal's entries. */
35
32
  journal?: JournalAuditOptions;
36
33
  tools: FeedbackAssessment;
37
34
  architecture: FeedbackAssessment;
@@ -1,6 +1,6 @@
1
1
  import { runRetrospectiveFactsReport, } from "../retrospective-facts/index.mjs";
2
2
  import { runRetrospectiveTranscriptReport, } from "../retrospective-transcript/index.mjs";
3
- import { assertSingleAuditSource, auditAssessed, loadAuditInputs, renderAuditReport, completeCoverageError, } from "./feedback-audit-source.mjs";
3
+ import { auditAssessed, loadAuditInputs, renderAuditReport, completeCoverageError, } from "./feedback-audit-source.mjs";
4
4
  import { AUDIT_ATTEMPTS, boundedAuditSource, shrinkCaps, UNBOUNDED_CAPS, } from "./feedback-compose-audit.mjs";
5
5
  import { createFeedbackEnvelope, feedbackOverflowBytes, redactFeedbackText, } from "./feedback-codec.mjs";
6
6
  import { validateFeedbackText } from "./feedback-fields.mjs";
@@ -33,7 +33,6 @@ function unavailable(marker, input) {
33
33
  return `${marker}\nStatus: ${input.status.replaceAll('-', ' ')} (${input.reason})`;
34
34
  }
35
35
  export async function composeRetrospective(input) {
36
- assertSingleAuditSource(input);
37
36
  const unavailableFacts = 'status' in input.facts
38
37
  ? {
39
38
  markdown: unavailable('=== Retrospective Facts ===', input.facts),
@@ -93,7 +92,7 @@ export async function composeRetrospective(input) {
93
92
  let audit;
94
93
  let markdown;
95
94
  for (let attempt = 1;; attempt++) {
96
- audit = renderAuditReport(input.sessionId, boundedAuditSource(input, caps), loaded);
95
+ audit = renderAuditReport(boundedAuditSource(input, caps), loaded);
97
96
  markdown = assemble(audit.markdown);
98
97
  const overflow = feedbackOverflowBytes(envelopeFor(markdown));
99
98
  if (!overflow || attempt === AUDIT_ATTEMPTS)
@@ -107,7 +106,5 @@ export async function composeRetrospective(input) {
107
106
  ['not-assessed', 'unavailable'].includes(input.architecture.status) ||
108
107
  !auditAssessed(audit)))
109
108
  throw completeCoverageError(input);
110
- if ((audit.coverage.droppedCount ?? 0) > input.feedbackCoverage.droppedCount)
111
- throw new Error('feedback coverage dropped count must include observed friction drops');
112
109
  return createFeedbackEnvelope(envelopeFor(markdown)).markdown;
113
110
  }
@@ -1,4 +1,4 @@
1
- import { isWellFormedUnicode } from "../session-friction/text.mjs";
1
+ import { isWellFormedUnicode } from "../audit-journal/text.mjs";
2
2
  export function validateFeedbackText(value, label, maximum) {
3
3
  if (typeof value !== 'string' ||
4
4
  !value.trim() ||
@@ -1,4 +1,4 @@
1
- import { isWellFormedUnicode } from "../session-friction/text.mjs";
1
+ import { isWellFormedUnicode } from "../audit-journal/text.mjs";
2
2
  import { assertSessionId } from "./session-id.mjs";
3
3
  import { isObject } from "./snapshot-partition-guards.mjs";
4
4
  export function validateFeedbackIdentity(identity) {
@@ -1,5 +1,5 @@
1
- import { type JournalScan } from '../session-friction/journal.mts';
2
- import type { JournalLoader, SessionFrictionCoverage } from '../session-friction/types.mts';
1
+ import { type JournalScan } from '../audit-journal/journal.mts';
2
+ import type { AuditCoverage, JournalLoader } from '../audit-journal/types.mts';
3
3
  export type JournalAuditOptions = {
4
4
  journalLoader: JournalLoader;
5
5
  /** Redacts raw field text before Markdown escaping, which would otherwise break matching. */
@@ -13,19 +13,17 @@ export type JournalAuditOptions = {
13
13
  /** `journal-only` says both sections were assessed from journal entries, with no log observed. */
14
14
  export type JournalAuditReport = {
15
15
  coverage: {
16
- journalStatus: SessionFrictionCoverage['journalStatus'];
17
- frictionStatus: 'journal-only';
16
+ journalStatus: AuditCoverage['journalStatus'];
18
17
  truncated: false;
19
- droppedCount?: never;
20
18
  };
21
19
  markdown: string;
22
20
  };
23
21
  /**
24
22
  * Builds `## CI Failures` and `## Sandbox & Permission Audit` from the session's journal entries
25
- * alone. Unlike `buildSessionFrictionReport`, a session with no journal is unavailable rather than
26
- * empty: without a log, nothing else establishes that the session was observed.
23
+ * alone. A session with no journal is unavailable rather than empty: nothing else establishes
24
+ * that the session was observed.
27
25
  */
28
26
  export declare function loadJournalAuditInput(sessionId: string, options: JournalAuditOptions): Promise<JournalScan>;
29
27
  /** Pure render of a loaded journal scan, so it can be repeated without reloading. */
30
- export declare function renderJournalAuditReport(sessionId: string, options: JournalAuditOptions, scanned: JournalScan): JournalAuditReport;
28
+ export declare function renderJournalAuditReport(options: JournalAuditOptions, scanned: JournalScan): JournalAuditReport;
31
29
  export declare function buildJournalAuditReport(sessionId: string, options: JournalAuditOptions): Promise<JournalAuditReport>;
@@ -1,24 +1,24 @@
1
- import { buildCiFailuresSection, getConformingGroups, incompleteCiSection, } from "../session-friction/ci-failures.mjs";
2
- import { scanJournal } from "../session-friction/journal.mjs";
3
- import { validateSessionId } from "../session-friction/session-id.mjs";
1
+ import { buildCiFailuresSection, getConformingGroups, incompleteCiSection, } from "../audit-journal/ci-failures.mjs";
2
+ import { scanJournal } from "../audit-journal/journal.mjs";
3
+ import { validateSessionId } from "../audit-journal/session-id.mjs";
4
4
  import { getConformingSandboxBlocks, journalSandboxSection, unavailableSandboxSection, } from "./feedback-journal-sandbox.mjs";
5
5
  function report(journalStatus, markdown) {
6
- return { coverage: { journalStatus, frictionStatus: 'journal-only', truncated: false }, markdown };
6
+ return { coverage: { journalStatus, truncated: false }, markdown };
7
7
  }
8
8
  function unavailable(reason) {
9
9
  return report('unavailable', `## CI Failures\nStatus: unavailable (${reason})\n\n${unavailableSandboxSection(reason)}`);
10
10
  }
11
11
  /**
12
12
  * Builds `## CI Failures` and `## Sandbox & Permission Audit` from the session's journal entries
13
- * alone. Unlike `buildSessionFrictionReport`, a session with no journal is unavailable rather than
14
- * empty: without a log, nothing else establishes that the session was observed.
13
+ * alone. A session with no journal is unavailable rather than empty: nothing else establishes
14
+ * that the session was observed.
15
15
  */
16
16
  export async function loadJournalAuditInput(sessionId, options) {
17
17
  validateSessionId(sessionId);
18
18
  return scanJournal(sessionId, options.journalLoader);
19
19
  }
20
20
  /** Pure render of a loaded journal scan, so it can be repeated without reloading. */
21
- export function renderJournalAuditReport(sessionId, options, scanned) {
21
+ export function renderJournalAuditReport(options, scanned) {
22
22
  if (scanned.status === 'unreachable')
23
23
  return unavailable('blackboard unreachable');
24
24
  if (scanned.status === 'not-found')
@@ -27,9 +27,8 @@ export function renderJournalAuditReport(sessionId, options, scanned) {
27
27
  const sandbox = getConformingSandboxBlocks(scanned.entries, options.redact, options.budgets?.sandboxBytes);
28
28
  if (scanned.truncated)
29
29
  return report('partial', `${incompleteCiSection(ci)}\n\n${unavailableSandboxSection('journal scan incomplete', sandbox)}`);
30
- const journal = { status: 'ok', markdownBlocks: ci, truncated: false };
31
- return report('complete', `${buildCiFailuresSection(sessionId, journal, 'journal-only')}\n\n${journalSandboxSection(sandbox)}`);
30
+ return report('complete', `${buildCiFailuresSection(ci)}\n\n${journalSandboxSection(sandbox)}`);
32
31
  }
33
32
  export async function buildJournalAuditReport(sessionId, options) {
34
- return renderJournalAuditReport(sessionId, options, await loadJournalAuditInput(sessionId, options));
33
+ return renderJournalAuditReport(options, await loadJournalAuditInput(sessionId, options));
35
34
  }
@@ -1,5 +1,5 @@
1
- import type { AuditRedactor } from '../session-friction/text.mts';
2
- import type { JournalEntry } from '../session-friction/types.mts';
1
+ import type { AuditRedactor } from '../audit-journal/text.mts';
2
+ import type { JournalEntry } from '../audit-journal/types.mts';
3
3
  /** Journal entries that hold exactly one sandbox or permission block, made paste-safe. */
4
4
  export declare function getConformingSandboxBlocks(entries: Iterable<JournalEntry>, redact?: AuditRedactor, maxBytes?: number): string[];
5
5
  export declare function unavailableSandboxSection(reason: string, blocks?: string[]): string;
@@ -1,6 +1,6 @@
1
- import { conformingBlocks, matchAuditBlock } from "../session-friction/audit-block.mjs";
1
+ import { conformingBlocks, matchAuditBlock } from "../audit-journal/audit-block.mjs";
2
2
  const SANDBOX_SECTION_HEADER = '## Sandbox & Permission Audit';
3
- const SOURCE = 'journal entries only; no friction log observed';
3
+ const SOURCE = 'journal entries only';
4
4
  const EVIDENCE = /^ {2}- Evidence: .*[^\s]$/;
5
5
  const DISPOSITION = /^ {2}- Disposition: .*[^\s]$/;
6
6
  const ESCALATION = [
@@ -1,6 +1,6 @@
1
1
  import { readdirSync, renameSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { withFileLock } from "../session-friction/lock.mjs";
3
+ import { withFileLock } from "../private-files/lock.mjs";
4
4
  import { canonicalFeedback } from "./feedback-canonical.mjs";
5
5
  import { ensureOutboxDirectory } from "./feedback-outbox-directory.mjs";
6
6
  import { filename, outboxDirectory, readRecord, records, syncDirectory, } from "./feedback-outbox.mjs";
@@ -3,8 +3,8 @@ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { closeSync, constants, fsyncSync, fstatSync, openSync, readdirSync, renameSync, unlinkSync, } from 'node:fs';
4
4
  import { isAbsolute, join, resolve } from 'node:path';
5
5
  import { ensureOutboxDirectory } from "./feedback-outbox-directory.mjs";
6
- import { withFileLock } from "../session-friction/lock.mjs";
7
- import { openLogFile, readLogContent, writeAll } from "../session-friction/log-file.mjs";
6
+ import { withFileLock } from "../private-files/lock.mjs";
7
+ import { openLogFile, readLogContent, writeAll } from "../private-files/log-file.mjs";
8
8
  import { validateFeedbackEnvelope } from "./feedback-codec.mjs";
9
9
  import { validateFeedbackIdentity } from "./feedback-identity.mjs";
10
10
  import { isObject } from "./snapshot-partition-guards.mjs";
@@ -23,5 +23,5 @@ export { feedbackOutboxStatus, readFeedbackOutbox } from './feedback-outbox.mts'
23
23
  export { composeRetrospective } from './feedback-compose.mts';
24
24
  export type { RetrospectiveCompositionInput, FeedbackAssessment } from './feedback-compose.mts';
25
25
  export type { JournalAuditOptions } from './feedback-journal-audit.mts';
26
- export type { JournalEntry, JournalLoader, JournalLoadResult } from '../session-friction/types.mts';
26
+ export type { JournalEntry, JournalLoader, JournalLoadResult } from '../audit-journal/types.mts';
27
27
  export { feedbackOutboxCounts } from './feedback-outbox-counts.mts';
@@ -0,0 +1,6 @@
1
+ import type { JournalEntry } from './types.mts';
2
+ import type { AuditRedactor } from './text.mts';
3
+ export declare function isConformingCiFailureBlock(markdown: string): boolean;
4
+ export declare function getConformingGroups(entries: Iterable<JournalEntry>, redact?: AuditRedactor, maxBytes?: number): string[];
5
+ export declare function incompleteCiSection(markdownBlocks: string[]): string;
6
+ export declare function buildCiFailuresSection(markdownBlocks: string[]): string;
@@ -1,5 +1,4 @@
1
1
  import { conformingBlocks, matchAuditBlock } from "./audit-block.mjs";
2
- import { markdownAuditText } from "./text.mjs";
3
2
  const GROUP_HEADER = /^- `(recurring|one-off)` — `GitHub Actions` — .*[^\s]$/;
4
3
  const EVIDENCE = /^ {2}- Evidence: .*[^\s]$/;
5
4
  const ROOT_DIAGNOSTIC = /^ {2}- Root diagnostic: .*[^\s]$/;
@@ -17,7 +16,6 @@ const CI_SECTION_MAX_BYTES = 4_000;
17
16
  // Field order: header, Evidence, Root diagnostic, Disposition. Cut Root diagnostic first and
18
17
  // Evidence last, so the run URL and commit SHA survive longest.
19
18
  const SHRINK_ORDER = [2, 3, 0, 1];
20
- const SESSION_ID_MAX_BYTES = 120;
21
19
  function matchBlock(markdown, render = {}) {
22
20
  return matchAuditBlock(markdown, [GROUP_HEADER, EVIDENCE, ROOT_DIAGNOSTIC, DISPOSITION], FIELD_PREFIXES, { ...render, shrinkOrder: SHRINK_ORDER });
23
21
  }
@@ -45,12 +43,8 @@ export function incompleteCiSection(markdownBlocks) {
45
43
  ...markdownBlocks,
46
44
  ].join('\n\n');
47
45
  }
48
- export function buildCiFailuresSection(sessionId, journal, frictionStatus) {
49
- if (journal.status === 'unreachable')
50
- return `${CI_FAILURES_HEADER}\nStatus: unavailable (blackboard unreachable)`;
51
- if (journal.markdownBlocks.length === 0 && frictionStatus === 'absent')
52
- return `${CI_FAILURES_HEADER}\nStatus: unavailable (no friction log for session ${markdownAuditText(sessionId, undefined, SESSION_ID_MAX_BYTES)})`;
53
- if (journal.markdownBlocks.length === 0)
46
+ export function buildCiFailuresSection(markdownBlocks) {
47
+ if (markdownBlocks.length === 0)
54
48
  return `${CI_FAILURES_HEADER}\nStatus: none observed`;
55
- return `${CI_FAILURES_HEADER}\nStatus: failures observed\n\n${journal.markdownBlocks.join('\n\n')}`;
49
+ return `${CI_FAILURES_HEADER}\nStatus: failures observed\n\n${markdownBlocks.join('\n\n')}`;
56
50
  }
@@ -9,6 +9,5 @@ export type JournalScan = {
9
9
  status: 'unreachable';
10
10
  diagnostic: string;
11
11
  };
12
- export declare function errorMessage(error: unknown): string;
13
12
  /** Loads the session journal through the caller's loader within the shared bounded scan. */
14
13
  export declare function scanJournal(sessionId: string, loader: JournalLoader): Promise<JournalScan>;
@@ -44,7 +44,7 @@ async function collectEntries(entries) {
44
44
  }
45
45
  return { entries: result, truncated };
46
46
  }
47
- export function errorMessage(error) {
47
+ function errorMessage(error) {
48
48
  try {
49
49
  return error instanceof Error ? error.message : String(error);
50
50
  }
@@ -0,0 +1 @@
1
+ export declare function validateSessionId(sessionId: string): void;
@@ -1,4 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { isWellFormedUnicode, normalizeAuditText } from "./text.mjs";
3
2
  const SESSION_ID_MAX_LENGTH = 4096;
4
3
  export function validateSessionId(sessionId) {
@@ -11,7 +10,3 @@ export function validateSessionId(sessionId) {
11
10
  if (normalizeAuditText(sessionId) === '')
12
11
  throw new Error('sessionId must be non-empty');
13
12
  }
14
- export function sanitizeSessionId(sessionId) {
15
- validateSessionId(sessionId);
16
- return createHash('sha256').update(sessionId).digest('hex');
17
- }
@@ -1,7 +1,5 @@
1
1
  export declare function normalizeAuditText(value: string): string;
2
- export declare function boundedText(value: string, maximum: number): string;
3
2
  export declare function isWellFormedUnicode(value: string): boolean;
4
- export declare function isSafeAuditText(value: unknown): value is string;
5
3
  export type AuditRedactor = (value: string) => string;
6
4
  /**
7
5
  * Splits a field into atomic Markdown-escaped units (one code point, plus its backslash when
@@ -14,5 +12,4 @@ export declare function markdownAuditUnits(value: string, redact?: AuditRedactor
14
12
  * Joins escaped units whole, or, when over `maxBytes`, keeps only complete whitespace-separated
15
13
  * tokens (so URLs and SHAs are never split) and marks the cut. Never ends in whitespace.
16
14
  */
17
- export declare function fitUnits(units: string[], maxBytes: number, atTokens?: boolean): string;
18
- export declare function markdownAuditText(value: string, redact?: AuditRedactor, maxBytes?: number): string;
15
+ export declare function fitUnits(units: string[], maxBytes: number): string;
@@ -3,14 +3,6 @@ const MARKDOWN_CHARACTER = /\\|`|\*|_|~|\[|\]|<|>|#|-|\+|!|\||&/;
3
3
  export function normalizeAuditText(value) {
4
4
  return value.replace(CONTROL_CHARACTERS, ' ').trim();
5
5
  }
6
- export function boundedText(value, maximum) {
7
- let end = Math.min(value.length, maximum);
8
- if (end < value.length &&
9
- /[\uD800-\uDBFF]/.test(value[end - 1]) &&
10
- /[\uDC00-\uDFFF]/.test(value[end]))
11
- end--;
12
- return value.slice(0, end);
13
- }
14
6
  export function isWellFormedUnicode(value) {
15
7
  for (let index = 0; index < value.length; index++) {
16
8
  const code = value.charCodeAt(index);
@@ -25,12 +17,6 @@ export function isWellFormedUnicode(value) {
25
17
  }
26
18
  return true;
27
19
  }
28
- export function isSafeAuditText(value) {
29
- return (typeof value === 'string' &&
30
- value !== '' &&
31
- isWellFormedUnicode(value) &&
32
- value === normalizeAuditText(value));
33
- }
34
20
  /**
35
21
  * Splits a field into atomic Markdown-escaped units (one code point, plus its backslash when
36
22
  * escaped), so a later cut can never land inside an escape sequence. Redaction runs on the raw
@@ -47,7 +33,7 @@ const WHITESPACE = /^\s$/u;
47
33
  * Joins escaped units whole, or, when over `maxBytes`, keeps only complete whitespace-separated
48
34
  * tokens (so URLs and SHAs are never split) and marks the cut. Never ends in whitespace.
49
35
  */
50
- export function fitUnits(units, maxBytes, atTokens = true) {
36
+ export function fitUnits(units, maxBytes) {
51
37
  const whole = units.join('');
52
38
  if (Buffer.byteLength(whole) <= maxBytes)
53
39
  return whole;
@@ -58,13 +44,9 @@ export function fitUnits(units, maxBytes, atTokens = true) {
58
44
  used += Buffer.byteLength(unit);
59
45
  if (used > room)
60
46
  break;
61
- if (!atTokens || WHITESPACE.test(unit))
62
- boundary = atTokens ? index : index + 1;
47
+ if (WHITESPACE.test(unit))
48
+ boundary = index;
63
49
  }
64
50
  const kept = units.slice(0, boundary).join('').trimEnd();
65
- return kept ? `${kept}${atTokens ? ' ' : ''}${CUT_MARKER}` : CUT_MARKER;
66
- }
67
- export function markdownAuditText(value, redact, maxBytes = Infinity) {
68
- // Used for identifiers, which have no token boundaries to cut at.
69
- return fitUnits(markdownAuditUnits(value, redact), maxBytes, false);
51
+ return kept ? `${kept} ${CUT_MARKER}` : CUT_MARKER;
70
52
  }
@@ -0,0 +1,17 @@
1
+ export type JournalEntry = {
2
+ data?: {
3
+ type?: unknown;
4
+ markdown?: unknown;
5
+ };
6
+ };
7
+ export type JournalLoadResult = {
8
+ status: 'ok';
9
+ entries: Iterable<JournalEntry> | AsyncIterable<JournalEntry>;
10
+ } | {
11
+ status: 'not-found';
12
+ };
13
+ export type JournalLoader = (sessionId: string) => JournalLoadResult | Promise<JournalLoadResult>;
14
+ export type AuditCoverage = {
15
+ journalStatus: 'complete' | 'partial' | 'unavailable';
16
+ truncated: boolean;
17
+ };
@@ -1,9 +1,11 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { ghSpawnEnv } from "../../gh-cli/env.mjs";
2
3
  import { auditCiJobRuntime } from "../../gha-runtime-audit/index.mjs";
3
4
  /* v8 ignore next 10 -- live `gh api` adapter */
4
5
  function defaultGhApiExecutor() {
5
6
  return async (request) => JSON.parse(execFileSync('gh', ['api', request.endpoint, '--jq', request.jq], {
6
7
  encoding: 'utf8',
8
+ env: ghSpawnEnv(),
7
9
  maxBuffer: 16 * 1024 * 1024,
8
10
  }));
9
11
  }