vouchington-tooling 0.23.0 → 0.24.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.
- package/README.md +76 -4
- package/dist/agent-blackboard/client.d.mts +34 -0
- package/dist/agent-blackboard/client.mjs +41 -0
- package/dist/agent-blackboard/feedback-canonical.d.mts +1 -0
- package/dist/agent-blackboard/feedback-canonical.mjs +13 -0
- package/dist/agent-blackboard/feedback-codec.d.mts +7 -0
- package/dist/agent-blackboard/feedback-codec.mjs +121 -0
- package/dist/agent-blackboard/feedback-compose.d.mts +37 -0
- package/dist/agent-blackboard/feedback-compose.mjs +109 -0
- package/dist/agent-blackboard/feedback-deadline.d.mts +1 -0
- package/dist/agent-blackboard/feedback-deadline.mjs +18 -0
- package/dist/agent-blackboard/feedback-delivery.d.mts +18 -0
- package/dist/agent-blackboard/feedback-delivery.mjs +88 -0
- package/dist/agent-blackboard/feedback-fields.d.mts +4 -0
- package/dist/agent-blackboard/feedback-fields.mjs +38 -0
- package/dist/agent-blackboard/feedback-identity.d.mts +2 -0
- package/dist/agent-blackboard/feedback-identity.mjs +29 -0
- package/dist/agent-blackboard/feedback-online-error.d.mts +7 -0
- package/dist/agent-blackboard/feedback-online-error.mjs +23 -0
- package/dist/agent-blackboard/feedback-online.d.mts +2 -0
- package/dist/agent-blackboard/feedback-online.mjs +137 -0
- package/dist/agent-blackboard/feedback-outbox.d.mts +8 -0
- package/dist/agent-blackboard/feedback-outbox.mjs +157 -0
- package/dist/agent-blackboard/feedback-types.d.mts +66 -0
- package/dist/agent-blackboard/feedback-types.mjs +1 -0
- package/dist/agent-blackboard/index.d.mts +16 -39
- package/dist/agent-blackboard/index.mjs +35 -81
- package/dist/cli/commands/agent-blackboard.mjs +35 -4
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +3 -1
- package/dist/retrospective-facts/coverage.d.mts +6 -0
- package/dist/retrospective-facts/coverage.mjs +42 -0
- package/dist/retrospective-facts/index.d.mts +3 -0
- package/dist/retrospective-facts/index.mjs +18 -1
- package/dist/retrospective-transcript/codex.mjs +8 -3
- package/dist/retrospective-transcript/index.d.mts +5 -0
- package/dist/retrospective-transcript/index.mjs +29 -9
- package/dist/session-friction/classify.mjs +25 -14
- package/dist/session-friction/coverage-ledger.d.mts +10 -0
- package/dist/session-friction/coverage-ledger.mjs +75 -0
- package/dist/session-friction/index.d.mts +1 -1
- package/dist/session-friction/log-file.d.mts +2 -2
- package/dist/session-friction/log-file.mjs +22 -10
- package/dist/session-friction/log.mjs +57 -10
- package/dist/session-friction/report.mjs +36 -7
- package/dist/session-friction/sandbox.mjs +11 -4
- package/dist/session-friction/types.d.mts +25 -4
- package/dist/workflow-policy/index.d.mts +1 -1
- package/dist/workflow-policy/index.mjs +1 -1
- package/dist/workflow-policy/permission-declarations.d.mts +14 -0
- package/dist/workflow-policy/permission-declarations.mjs +55 -0
- package/dist/workflow-policy/permission-inheritance.d.mts +8 -0
- package/dist/workflow-policy/permission-inheritance.mjs +88 -0
- package/dist/workflow-policy/permission-mismatches.d.mts +4 -1
- package/dist/workflow-policy/permission-mismatches.mjs +14 -3
- package/docs/workflow-and-compiler.md +18 -1
- package/package.json +1 -1
- package/skills/blackboard/SKILL.md +34 -14
- package/skills/retrospective/SKILL.md +27 -16
- package/skills/retrospective-distill/SKILL.md +15 -6
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ vouchington wait-for-apt-locks
|
|
|
66
66
|
vouchington retrospective-transcript --jsonl /path/to/transcript.jsonl
|
|
67
67
|
vouchington retrospective-facts --pr 49 --repo vouchington/vouchington-infra --raw
|
|
68
68
|
vouchington agent-blackboard probe
|
|
69
|
-
vouchington agent-blackboard journal append --session-id <uuid> --agent codex --version 1 --file note.md --repository
|
|
69
|
+
vouchington agent-blackboard journal append --session-id <uuid> --agent codex --version 1 --file note.md --repository owner/repo --mode autonomous --source-event-id event:1 --work-outcome in-progress --coverage-status partial --timestamp 2026-01-01T00:00:00.000Z
|
|
70
70
|
vouchington agent-blackboard journal entries --session-id <uuid>
|
|
71
71
|
vouchington agent-blackboard snapshot partition --snapshot <snapshot.jsonl> --checksum <sha256> --counts <counts.json>
|
|
72
72
|
vouchington agent-blackboard snapshot cleanup --snapshot <snapshot.jsonl> --partition-directory <partitions-dir> --receipt <receipt-json>
|
|
@@ -147,7 +147,10 @@ as not merged to main, and a missing base is unavailable.
|
|
|
147
147
|
|
|
148
148
|
Agent Blackboard support is optional: only the `agent-blackboard` subpath and its CLI commands
|
|
149
149
|
need `agent-blackboard@^0.6.0`. Snapshot cleanup accepts only package-generated temporary paths.
|
|
150
|
-
`appendJournal`
|
|
150
|
+
`appendJournal` and the journal CLI now require explicit mode, stable source-event identity, work
|
|
151
|
+
outcome, feedback coverage, and exact repositories. This is a breaking pre-1.0 API change; adopt the
|
|
152
|
+
shared contract before upgrading consumers. `appendJournal` reads a bounded UTF-8 note and delegates
|
|
153
|
+
to `writeFeedback`, returning delivered or pending state rather than a success-shaped message. The CLI accepts one or more
|
|
151
154
|
`--repository owner/name` flags; it records the entry's exact repositories and updates the session's
|
|
152
155
|
cumulative repository list before appending.
|
|
153
156
|
Programmatic callers launched from a different workspace directory pass their own module URL as
|
|
@@ -405,13 +408,19 @@ absolute log directory, host-independent observation, and journal loader; it doe
|
|
|
405
408
|
environment variables, install hooks, or connect to a journal service by itself. Invoking
|
|
406
409
|
`recordFriction` touches the session log even when no event is classified, preserving the
|
|
407
410
|
difference between an observed clean session and missing evidence. Report markdown keeps backend
|
|
408
|
-
diagnostics separate from its paste-safe output. Capture stores at most 500 events per session
|
|
411
|
+
diagnostics separate from its paste-safe output. Capture stores at most 500 events per session and counts rejected captures in a fixed-size atomic
|
|
412
|
+
sidecar under the same session lock for either the event or byte limit, with visible `truncated`/`droppedCount` metadata. Saturated
|
|
413
|
+
capture reads only the bounded counter; reports also preserve historical overflow markers.
|
|
414
|
+
Interrupted or inconsistent counter updates make coverage unavailable. Malformed records also make the scan partial. It
|
|
409
415
|
truncates event detail to 1,000 characters, and consumes up to 500 entries from the journal loader
|
|
410
416
|
when building a report, stopping earlier when its aggregate 1 MB inspected-byte budget is reached.
|
|
411
417
|
Bounded journal scans that stop before exhaustion are reported as incomplete rather than clean.
|
|
412
418
|
Report liveness inherits the caller-supplied journal loader, which must bound its own I/O and yields.
|
|
413
419
|
Log reads are capped at 2 MB, journal Markdown at 10,000 bytes per entry,
|
|
414
|
-
and rendered audit fields at 120 escaped characters.
|
|
420
|
+
and rendered audit fields at 120 escaped characters. Permission requests record requested outcomes; explicit result observations may record approved
|
|
421
|
+
or denied outcomes. A decision and simultaneous tool failure are both retained and counted in
|
|
422
|
+
the report. Escalation detail without a decision remains unknown. Localhost connection
|
|
423
|
+
refusal is an ambiguous failure and does not establish a sandbox cause. The supplied log directory must be dedicated
|
|
415
424
|
to session-friction; existing directories must already be owner-only, while newly created
|
|
416
425
|
directories and log files are enforced as owner-only when recording. Reads use a fixed bounded
|
|
417
426
|
buffer that can detect growth one byte beyond the documented 2 MB cap.
|
|
@@ -506,3 +515,66 @@ links declared prerequisites. Without a manifest, it links the conventional
|
|
|
506
515
|
outside either root, and existing non-matching destinations.
|
|
507
516
|
`targetRoot` must already exist as a physical directory path: symlinked target roots or ancestors are
|
|
508
517
|
rejected.
|
|
518
|
+
|
|
519
|
+
## Validated feedback and delivery
|
|
520
|
+
|
|
521
|
+
Import `createFeedbackEnvelope`, `validateFeedbackEnvelope`, `writeFeedback`, `verifyFreshFeedback`,
|
|
522
|
+
`composeRetrospective`, `feedbackOutboxStatus`, and `flushFeedbackOutbox` from the existing
|
|
523
|
+
`vouchington-tooling/agent-blackboard` subpath. Consumers must run Node 24 or newer and install `agent-blackboard@^0.6.0` explicitly alongside
|
|
524
|
+
`vouchington-tooling`; the provider is not installed by this package. Missing provider installation
|
|
525
|
+
returns `client-unavailable`; missing or invalid connection configuration returns
|
|
526
|
+
`configuration-invalid`, separately from transport outages and authentication rejection.
|
|
527
|
+
The upstream `agent-blackboard` client still owns the
|
|
528
|
+
service protocol; this integration adds no provider schema or service. Supply consumer module
|
|
529
|
+
context through `dependencies.resolveFrom` or inject the client loader at the transport boundary.
|
|
530
|
+
|
|
531
|
+
The version-one envelope retains canonical `markdown` and storage `type: journal | retrospective`.
|
|
532
|
+
It requires exact canonical repositories, stable URL-safe `sourceEventId`, ISO timestamp, explicit
|
|
533
|
+
`workOutcome`, and `feedbackCoverage` with status, sources, and dropped count. Optional category is
|
|
534
|
+
context only. Retrospectives retain valid date and integer/string issue/PR references. Markdown is
|
|
535
|
+
capped at 12 KB and the envelope at 16 KB. Known credentials are redacted; authors remain responsible
|
|
536
|
+
for minimizing summaries and excluding raw logs, transcripts, environment dumps, and undisclosed
|
|
537
|
+
secrets. A bound violation fails visibly without clipping findings.
|
|
538
|
+
|
|
539
|
+
`writeFeedback({identity, envelope, mode, ...})` accepts an exact caller-owned session identity and
|
|
540
|
+
never manufactures native agent identities. Interactive mode requires an absolute private outbox
|
|
541
|
+
directory. The writer persists before transport with atomic rename, file/directory fsync, a stable
|
|
542
|
+
record path, and owner-only permissions. The outbox holds at most 128 records and 2 MB. Saturation,
|
|
543
|
+
corruption, or persistence failure blocks capture and preserves unsent records. Delivered records
|
|
544
|
+
are removed only after matching readback. Pending state includes count and a safe diagnostic;
|
|
545
|
+
interactive CLI exit zero means durable retention, and callers must inspect the printed status to
|
|
546
|
+
know whether delivery was acknowledged. Autonomous errors and unpersisted writes fail nonzero.
|
|
547
|
+
Identity, content, and archived-session conflicts fail in either mode while preserving the unsent
|
|
548
|
+
record; they are not reported as transient pending delivery. Replay retains permanently conflicted
|
|
549
|
+
records and continues to deliver unrelated pending records; transient failures stop that replay.
|
|
550
|
+
Verified delivery retains its receipt when local cleanup fails, returning
|
|
551
|
+
`cleanupDiagnostic` with value `"outbox-cleanup-failed"`. A retained or crash-reappearing record is safe to replay with exact dedup.
|
|
552
|
+
Use `vouchington agent-blackboard journal flush --outbox-directory PATH` to replay unchanged pending
|
|
553
|
+
records after connectivity returns.
|
|
554
|
+
|
|
555
|
+
Autonomous mode prohibits the filesystem outbox. `verifyFreshFeedback` requires a fresh source-event
|
|
556
|
+
write plus readback of its own append receipt and rejects visible duplicate source records. This
|
|
557
|
+
is fresh online reporting evidence, not a distributed execution lease: eventually consistent
|
|
558
|
+
provider reads cannot establish source uniqueness. Trusted controllers
|
|
559
|
+
supply a unique new authorization-probe source ID, preserve their authoritative run/session
|
|
560
|
+
lineage, and atomically claim and revalidate the attempt before execution. Terminal writes keep work outcome separate from coverage and returned delivery state.
|
|
561
|
+
The delivered result includes a verified receipt with session ID, source-event ID and creation time.
|
|
562
|
+
Controllers require this online result together with atomically established attempt ownership
|
|
563
|
+
before execution.
|
|
564
|
+
|
|
565
|
+
Delivery is at least once. Replays pre-read matching source identity/content, accept equivalent
|
|
566
|
+
persistent duplicates, and reject conflicting reuse. Equality ignores object property order while
|
|
567
|
+
preserving array order. Each online operation has a 20-second deadline (`timeoutMs` can shorten it);
|
|
568
|
+
readback scans at most 10,000 records or 2 MB. The SDK does not expose cancellation for in-flight
|
|
569
|
+
requests, so a timed-out write can commit later. That never grants admission or a delivery receipt;
|
|
570
|
+
replay the identical envelope and source ID to verify its actual persistence.
|
|
571
|
+
|
|
572
|
+
`composeRetrospective` invokes the existing fact/transcript collectors with raw output disabled and
|
|
573
|
+
generates required sections and markers. Tool and architectural assessments preserve observation,
|
|
574
|
+
evidence, disposition and tracking reference. None-observed and unassessed states need a scoped
|
|
575
|
+
reason. Complete coverage is rejected when required sources are unavailable or capture is partial.
|
|
576
|
+
Typed collector reports expose availability independently of rendered text. Composition rejects
|
|
577
|
+
aggregate dropped counts below observed friction drops and retains larger caller aggregates without
|
|
578
|
+
double counting. Generated frontmatter carries validated `work_outcome` and `feedback_coverage`
|
|
579
|
+
for consumers to preserve or reject conflicting explicit overrides; trusted mode stays external.
|
|
580
|
+
Human top-five summaries do not authorize dropping additional durable findings.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type BlackboardConnection = {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
token: string;
|
|
4
|
+
readRetry: Record<string, never>;
|
|
5
|
+
};
|
|
6
|
+
export type BlackboardClientModule = {
|
|
7
|
+
Sessions: new (connection: BlackboardConnection) => {
|
|
8
|
+
ensure(input: unknown): Promise<{
|
|
9
|
+
status: 'created' | 'exists';
|
|
10
|
+
session: {
|
|
11
|
+
data: Record<string, unknown>;
|
|
12
|
+
archivedAt?: string | null;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
patch(input: unknown): Promise<unknown>;
|
|
16
|
+
list(input: unknown): Promise<unknown>;
|
|
17
|
+
get(id: string): Promise<unknown>;
|
|
18
|
+
};
|
|
19
|
+
Entries: new (connection: BlackboardConnection) => {
|
|
20
|
+
append(input: unknown): Promise<{
|
|
21
|
+
createdAt: string;
|
|
22
|
+
data: unknown;
|
|
23
|
+
}>;
|
|
24
|
+
get(input: unknown): AsyncIterable<unknown>;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
type BlackboardClientLoader = () => Promise<BlackboardClientModule>;
|
|
28
|
+
export type BlackboardClientDependencies = {
|
|
29
|
+
loadClient?: BlackboardClientLoader;
|
|
30
|
+
resolveFrom?: string | URL;
|
|
31
|
+
};
|
|
32
|
+
export declare function resolveBlackboardConnection(env?: NodeJS.ProcessEnv): BlackboardConnection;
|
|
33
|
+
export declare function loadClient(dependencies?: BlackboardClientDependencies): Promise<BlackboardClientModule>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
export function resolveBlackboardConnection(env = process.env) {
|
|
13
|
+
const baseUrl = env.AGENT_BLACKBOARD_URL;
|
|
14
|
+
const token = env.AGENT_BLACKBOARD_TOKEN;
|
|
15
|
+
if (!baseUrl)
|
|
16
|
+
throw new Error('AGENT_BLACKBOARD_URL is not set');
|
|
17
|
+
if (!token)
|
|
18
|
+
throw new Error('AGENT_BLACKBOARD_TOKEN is not set');
|
|
19
|
+
return { baseUrl, token, readRetry: {} };
|
|
20
|
+
}
|
|
21
|
+
export async function loadClient(dependencies = {}) {
|
|
22
|
+
const loader = dependencies.loadClient ?? (() => defaultClientLoader(dependencies.resolveFrom));
|
|
23
|
+
try {
|
|
24
|
+
return await loader();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (isMissingModuleError(error))
|
|
28
|
+
throw new Error('agent-blackboard is not installed; install it alongside vouchington-tooling to use this integration', { cause: error });
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function defaultClientLoader(resolveFrom) {
|
|
33
|
+
const consumerRequire = createRequire(resolveFrom ?? resolve(process.cwd(), 'package.json'));
|
|
34
|
+
const specifier = pathToFileURL(consumerRequire.resolve('agent-blackboard')).href;
|
|
35
|
+
return (await import(__rewriteRelativeImportExtension(specifier)));
|
|
36
|
+
}
|
|
37
|
+
function isMissingModuleError(error) {
|
|
38
|
+
return (error instanceof Error &&
|
|
39
|
+
'code' in error &&
|
|
40
|
+
(error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND'));
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function canonicalFeedback(value: unknown): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isObject } from './snapshot-partition-guards.mjs';
|
|
2
|
+
export function canonicalFeedback(value) {
|
|
3
|
+
return JSON.stringify(ordered(value));
|
|
4
|
+
}
|
|
5
|
+
function ordered(value) {
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return value.map(ordered);
|
|
8
|
+
if (!isObject(value))
|
|
9
|
+
return value;
|
|
10
|
+
return Object.fromEntries(Object.keys(value)
|
|
11
|
+
.sort()
|
|
12
|
+
.map((key) => [key, ordered(value[key])]));
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { normalizeRepositories } from './feedback-fields.mts';
|
|
2
|
+
import type { FeedbackEnvelope } from './feedback-types.mts';
|
|
3
|
+
export declare function validateFeedbackEnvelope(value: unknown): asserts value is FeedbackEnvelope;
|
|
4
|
+
export declare function redactFeedbackText(value: string, knownSensitiveValues?: string[]): string;
|
|
5
|
+
export declare function createFeedbackEnvelope(input: FeedbackEnvelope, options?: {
|
|
6
|
+
knownSensitiveValues?: string[];
|
|
7
|
+
}): FeedbackEnvelope;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { normalizeRepositories, validateFeedbackText, validateFeedbackReferences, } from './feedback-fields.mjs';
|
|
2
|
+
export { normalizeRepositories } from './feedback-fields.mjs';
|
|
3
|
+
import { isCount, isObject } from './snapshot-partition-guards.mjs';
|
|
4
|
+
const FEEDBACK_MAX_BYTES = 16_384;
|
|
5
|
+
const OUTCOMES = [
|
|
6
|
+
'in-progress',
|
|
7
|
+
'success',
|
|
8
|
+
'failure',
|
|
9
|
+
'cancelled',
|
|
10
|
+
'timed-out',
|
|
11
|
+
'no-change',
|
|
12
|
+
'policy-refusal',
|
|
13
|
+
'unknown',
|
|
14
|
+
];
|
|
15
|
+
const COVERAGE = ['complete', 'partial', 'unavailable', 'not-assessed', 'not-started'];
|
|
16
|
+
const FIELDS = [
|
|
17
|
+
'schemaVersion',
|
|
18
|
+
'type',
|
|
19
|
+
'sourceEventId',
|
|
20
|
+
'timestamp',
|
|
21
|
+
'repositories',
|
|
22
|
+
'markdown',
|
|
23
|
+
'workOutcome',
|
|
24
|
+
'feedbackCoverage',
|
|
25
|
+
'category',
|
|
26
|
+
'date',
|
|
27
|
+
'issues',
|
|
28
|
+
'prs',
|
|
29
|
+
];
|
|
30
|
+
export function validateFeedbackEnvelope(value) {
|
|
31
|
+
if (!isObject(value) ||
|
|
32
|
+
value.schemaVersion !== 1 ||
|
|
33
|
+
typeof value.type !== 'string' ||
|
|
34
|
+
!['journal', 'retrospective'].includes(value.type))
|
|
35
|
+
throw new Error('invalid feedback storage type or schemaVersion');
|
|
36
|
+
if (Object.keys(value).some((key) => !FIELDS.includes(key)))
|
|
37
|
+
throw new Error('unsupported feedback field');
|
|
38
|
+
validateFeedbackText(value.sourceEventId, 'sourceEventId', 256);
|
|
39
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(value.sourceEventId))
|
|
40
|
+
throw new Error('sourceEventId must be URL-safe');
|
|
41
|
+
validateFeedbackText(value.timestamp, 'timestamp', 40);
|
|
42
|
+
if (!Number.isFinite(Date.parse(value.timestamp)) ||
|
|
43
|
+
new Date(value.timestamp).toISOString() !== value.timestamp)
|
|
44
|
+
throw new Error('timestamp must be a canonical ISO date-time');
|
|
45
|
+
if (JSON.stringify(value.repositories) !== JSON.stringify(normalizeRepositories(value.repositories)))
|
|
46
|
+
throw new Error('repositories must be canonical');
|
|
47
|
+
validateFeedbackText(value.markdown, 'markdown', 12_000);
|
|
48
|
+
if (typeof value.workOutcome !== 'string' || !OUTCOMES.includes(value.workOutcome))
|
|
49
|
+
throw new Error('workOutcome must be explicit');
|
|
50
|
+
if (!isObject(value.feedbackCoverage) ||
|
|
51
|
+
typeof value.feedbackCoverage.status !== 'string' ||
|
|
52
|
+
!COVERAGE.includes(value.feedbackCoverage.status) ||
|
|
53
|
+
!isCount(value.feedbackCoverage.droppedCount))
|
|
54
|
+
throw new Error('feedbackCoverage must be explicit');
|
|
55
|
+
if (Object.keys(value.feedbackCoverage).some((key) => !['status', 'sources', 'droppedCount'].includes(key)))
|
|
56
|
+
throw new Error('unsupported feedbackCoverage field');
|
|
57
|
+
if (!Array.isArray(value.feedbackCoverage.sources) || value.feedbackCoverage.sources.length > 32)
|
|
58
|
+
throw new Error('coverage sources must be bounded');
|
|
59
|
+
for (const source of value.feedbackCoverage.sources)
|
|
60
|
+
validateFeedbackText(source, 'coverage source', 160);
|
|
61
|
+
if (value.feedbackCoverage.status === 'complete' && value.feedbackCoverage.droppedCount !== 0)
|
|
62
|
+
throw new Error('complete coverage cannot have dropped records');
|
|
63
|
+
if (value.category !== undefined)
|
|
64
|
+
validateFeedbackText(value.category, 'category', 160);
|
|
65
|
+
if (value.type === 'retrospective' || value.date !== undefined) {
|
|
66
|
+
if (typeof value.date !== 'string' ||
|
|
67
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(value.date) ||
|
|
68
|
+
!Number.isFinite(Date.parse(value.date)) ||
|
|
69
|
+
new Date(value.date).toISOString().slice(0, 10) !== value.date)
|
|
70
|
+
throw new Error('date must be a valid YYYY-MM-DD');
|
|
71
|
+
}
|
|
72
|
+
if (value.type === 'retrospective' || value.issues !== undefined)
|
|
73
|
+
validateFeedbackReferences(value.issues, 'issues');
|
|
74
|
+
if (value.type === 'retrospective' || value.prs !== undefined)
|
|
75
|
+
validateFeedbackReferences(value.prs, 'prs');
|
|
76
|
+
if (Buffer.byteLength(JSON.stringify(value)) > FEEDBACK_MAX_BYTES)
|
|
77
|
+
throw new Error('feedback envelope is too large');
|
|
78
|
+
}
|
|
79
|
+
export function redactFeedbackText(value, knownSensitiveValues = []) {
|
|
80
|
+
let result = value;
|
|
81
|
+
for (const secret of knownSensitiveValues)
|
|
82
|
+
if (secret.length >= 4)
|
|
83
|
+
result = result.split(secret).join('[REDACTED]');
|
|
84
|
+
return result
|
|
85
|
+
.replace(/\b(?:abb_sk_|gh[pousr]_|github_pat_|sk-(?:proj-)?)?[A-Za-z0-9_-]{30,}\b/g, (match) => /^(?:abb_sk_|gh[pousr]_|github_pat_|sk-)/.test(match) ? '[REDACTED]' : match)
|
|
86
|
+
.replace(/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, '[REDACTED]')
|
|
87
|
+
.replace(/(Bearer\s+)[^\s]+/gi, '$1[REDACTED]')
|
|
88
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[REDACTED]@')
|
|
89
|
+
.replace(/((?:password|secret|token|api[_-]?key)\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
|
|
90
|
+
}
|
|
91
|
+
export function createFeedbackEnvelope(input, options = {}) {
|
|
92
|
+
validateFeedbackText(input.markdown, 'markdown', 12_000);
|
|
93
|
+
validateFeedbackText(input.timestamp, 'timestamp', 40);
|
|
94
|
+
const envelope = {
|
|
95
|
+
...input,
|
|
96
|
+
repositories: normalizeRepositories(input.repositories),
|
|
97
|
+
markdown: redactFeedbackText(input.markdown, options.knownSensitiveValues),
|
|
98
|
+
timestamp: Number.isFinite(Date.parse(input.timestamp))
|
|
99
|
+
? new Date(input.timestamp).toISOString()
|
|
100
|
+
: input.timestamp,
|
|
101
|
+
feedbackCoverage: {
|
|
102
|
+
...input.feedbackCoverage,
|
|
103
|
+
sources: input.feedbackCoverage.sources.map((source) => redactFeedbackText(source, options.knownSensitiveValues)),
|
|
104
|
+
},
|
|
105
|
+
...(input.category === undefined
|
|
106
|
+
? {}
|
|
107
|
+
: { category: redactFeedbackText(input.category, options.knownSensitiveValues) }),
|
|
108
|
+
...(input.issues === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: {
|
|
111
|
+
issues: input.issues.map((ref) => typeof ref === 'string' ? redactFeedbackText(ref, options.knownSensitiveValues) : ref),
|
|
112
|
+
}),
|
|
113
|
+
...(input.prs === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: {
|
|
116
|
+
prs: input.prs.map((ref) => typeof ref === 'string' ? redactFeedbackText(ref, options.knownSensitiveValues) : ref),
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
validateFeedbackEnvelope(envelope);
|
|
120
|
+
return envelope;
|
|
121
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type RetrospectiveFactsOptions } from '../retrospective-facts/index.mts';
|
|
2
|
+
import { type ResolveOptions } from '../retrospective-transcript/index.mts';
|
|
3
|
+
import { type SessionFrictionReportOptions } from '../session-friction/index.mts';
|
|
4
|
+
import type { FeedbackCoverage, FeedbackReference, WorkOutcome } from './feedback-types.mts';
|
|
5
|
+
export type FeedbackAssessment = {
|
|
6
|
+
status: 'findings' | 'none-observed' | 'not-assessed' | 'unavailable';
|
|
7
|
+
findings?: Array<{
|
|
8
|
+
observation: string;
|
|
9
|
+
evidence: string;
|
|
10
|
+
disposition: string;
|
|
11
|
+
trackingReference?: string;
|
|
12
|
+
}>;
|
|
13
|
+
reason?: string;
|
|
14
|
+
};
|
|
15
|
+
type Unassessed = {
|
|
16
|
+
status: 'unavailable' | 'not-assessed';
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
export type RetrospectiveCompositionInput = {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
date: string;
|
|
22
|
+
issues: FeedbackReference[];
|
|
23
|
+
prs: FeedbackReference[];
|
|
24
|
+
description: string;
|
|
25
|
+
repositories: string[];
|
|
26
|
+
workOutcome: WorkOutcome;
|
|
27
|
+
feedbackCoverage: FeedbackCoverage;
|
|
28
|
+
narrative: string;
|
|
29
|
+
facts: Omit<RetrospectiveFactsOptions, 'raw'> | Unassessed;
|
|
30
|
+
transcript: ResolveOptions | Unassessed;
|
|
31
|
+
friction?: SessionFrictionReportOptions;
|
|
32
|
+
tools: FeedbackAssessment;
|
|
33
|
+
architecture: FeedbackAssessment;
|
|
34
|
+
knownSensitiveValues?: string[];
|
|
35
|
+
};
|
|
36
|
+
export declare function composeRetrospective(input: RetrospectiveCompositionInput): Promise<string>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { runRetrospectiveFactsReport, } from '../retrospective-facts/index.mjs';
|
|
2
|
+
import { runRetrospectiveTranscriptReport, } from '../retrospective-transcript/index.mjs';
|
|
3
|
+
import { buildSessionFrictionReport, } from '../session-friction/index.mjs';
|
|
4
|
+
import { createFeedbackEnvelope, redactFeedbackText } from './feedback-codec.mjs';
|
|
5
|
+
import { validateFeedbackText } from './feedback-fields.mjs';
|
|
6
|
+
function assessment(title, value) {
|
|
7
|
+
if (!['findings', 'none-observed', 'not-assessed', 'unavailable'].includes(value.status))
|
|
8
|
+
throw new Error('assessment status must be explicit');
|
|
9
|
+
if (value.status !== 'findings' &&
|
|
10
|
+
(typeof value.reason !== 'string' ||
|
|
11
|
+
!value.reason.trim() ||
|
|
12
|
+
Buffer.byteLength(value.reason) > 240))
|
|
13
|
+
throw new Error('assessment reason and inspected scope must be explicit and bounded');
|
|
14
|
+
const findings = value.findings ?? [];
|
|
15
|
+
if (findings.length > 20 || (value.status === 'findings') !== Boolean(findings.length))
|
|
16
|
+
throw new Error('assessment findings must match status and remain bounded');
|
|
17
|
+
const lines = findings.map((finding) => {
|
|
18
|
+
for (const field of [finding.observation, finding.evidence, finding.disposition])
|
|
19
|
+
if (typeof field !== 'string' || !field.trim() || Buffer.byteLength(field) > 1000)
|
|
20
|
+
throw new Error('assessment finding must contain bounded observation, evidence and disposition');
|
|
21
|
+
if (finding.trackingReference !== undefined)
|
|
22
|
+
validateFeedbackText(finding.trackingReference, 'assessment tracking reference', 240);
|
|
23
|
+
return `- ${finding.observation}\n - Evidence: ${finding.evidence}\n - Disposition: ${finding.disposition}${finding.trackingReference ? `\n - Tracking: ${finding.trackingReference}` : ''}`;
|
|
24
|
+
});
|
|
25
|
+
return `## ${title}\nStatus: ${value.status.replaceAll('-', ' ')}${value.reason ? ` (${value.reason})` : ''}\n${lines.join('\n')}`;
|
|
26
|
+
}
|
|
27
|
+
function unavailable(marker, input) {
|
|
28
|
+
if (input.status !== 'unavailable' && input.status !== 'not-assessed')
|
|
29
|
+
throw new Error('unassessed status must be unavailable or not-assessed');
|
|
30
|
+
if (!input.reason.trim() || Buffer.byteLength(input.reason) > 240)
|
|
31
|
+
throw new Error('unavailable reason must be bounded and explicit');
|
|
32
|
+
return `${marker}\nStatus: ${input.status.replaceAll('-', ' ')} (${input.reason})`;
|
|
33
|
+
}
|
|
34
|
+
export async function composeRetrospective(input) {
|
|
35
|
+
const unavailableFacts = 'status' in input.facts
|
|
36
|
+
? {
|
|
37
|
+
markdown: unavailable('=== Retrospective Facts ===', input.facts),
|
|
38
|
+
coverage: input.facts.status,
|
|
39
|
+
}
|
|
40
|
+
: undefined;
|
|
41
|
+
const unavailableTranscript = 'status' in input.transcript
|
|
42
|
+
? {
|
|
43
|
+
markdown: unavailable('=== Transcript Facts ===', input.transcript),
|
|
44
|
+
coverage: input.transcript.status,
|
|
45
|
+
}
|
|
46
|
+
: undefined;
|
|
47
|
+
const [facts, transcript, friction] = await Promise.all([
|
|
48
|
+
'status' in input.facts
|
|
49
|
+
? Promise.resolve(unavailableFacts)
|
|
50
|
+
: runRetrospectiveFactsReport({ ...input.facts, raw: false }),
|
|
51
|
+
'status' in input.transcript
|
|
52
|
+
? Promise.resolve(unavailableTranscript)
|
|
53
|
+
: runRetrospectiveTranscriptReport(input.transcript),
|
|
54
|
+
input.friction
|
|
55
|
+
? buildSessionFrictionReport(input.sessionId, input.friction)
|
|
56
|
+
: Promise.resolve({
|
|
57
|
+
coverage: {
|
|
58
|
+
journalStatus: 'unavailable',
|
|
59
|
+
frictionStatus: 'absent',
|
|
60
|
+
truncated: false,
|
|
61
|
+
},
|
|
62
|
+
markdown: '## CI Failures\nStatus: unavailable (not assessed)\n\n## Sandbox & Permission Audit\nStatus: unavailable (not assessed)',
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
if (input.feedbackCoverage.status === 'complete' &&
|
|
66
|
+
(facts.coverage !== 'complete' ||
|
|
67
|
+
transcript.coverage !== 'complete' ||
|
|
68
|
+
['not-assessed', 'unavailable'].includes(input.tools.status) ||
|
|
69
|
+
['not-assessed', 'unavailable'].includes(input.architecture.status) ||
|
|
70
|
+
friction.coverage.journalStatus !== 'complete' ||
|
|
71
|
+
!['empty', 'events'].includes(friction.coverage.frictionStatus) ||
|
|
72
|
+
friction.coverage.truncated))
|
|
73
|
+
throw new Error('complete feedback coverage requires assessed available factual and finding sources');
|
|
74
|
+
if ((friction.coverage.droppedCount ?? 0) > input.feedbackCoverage.droppedCount)
|
|
75
|
+
throw new Error('feedback coverage dropped count must include observed friction drops');
|
|
76
|
+
const markdown = [
|
|
77
|
+
'---',
|
|
78
|
+
`date: ${JSON.stringify(input.date)}`,
|
|
79
|
+
`issues: ${JSON.stringify(input.issues)}`,
|
|
80
|
+
`prs: ${JSON.stringify(input.prs)}`,
|
|
81
|
+
`session_id: ${JSON.stringify(input.sessionId)}`,
|
|
82
|
+
`description: ${JSON.stringify(input.description)}`,
|
|
83
|
+
`work_outcome: ${JSON.stringify(input.workOutcome)}`,
|
|
84
|
+
`feedback_coverage: ${JSON.stringify(input.feedbackCoverage)}`,
|
|
85
|
+
'---',
|
|
86
|
+
'',
|
|
87
|
+
input.narrative,
|
|
88
|
+
`## Outcome\nWork outcome: ${input.workOutcome}\nFeedback coverage: ${input.feedbackCoverage.status}\nDropped records: ${input.feedbackCoverage.droppedCount}`,
|
|
89
|
+
`## Verifiable Facts\n${facts.markdown.trim()}`,
|
|
90
|
+
`## Transcript Facts\n${transcript.markdown.trim()}`,
|
|
91
|
+
friction.markdown,
|
|
92
|
+
assessment('Tool Findings', input.tools),
|
|
93
|
+
assessment('Architecture Findings', input.architecture),
|
|
94
|
+
].join('\n\n');
|
|
95
|
+
const envelope = createFeedbackEnvelope({
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
type: 'retrospective',
|
|
98
|
+
sourceEventId: 'composition-validation',
|
|
99
|
+
timestamp: `${input.date}T00:00:00.000Z`,
|
|
100
|
+
date: input.date,
|
|
101
|
+
issues: input.issues,
|
|
102
|
+
prs: input.prs,
|
|
103
|
+
repositories: input.repositories,
|
|
104
|
+
markdown: redactFeedbackText(markdown, input.knownSensitiveValues),
|
|
105
|
+
workOutcome: input.workOutcome,
|
|
106
|
+
feedbackCoverage: input.feedbackCoverage,
|
|
107
|
+
});
|
|
108
|
+
return envelope.markdown;
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function feedbackDeadline<T>(operation: () => Promise<T>, timeoutMs?: number): Promise<T>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FeedbackDeliveryError } from './feedback-online-error.mjs';
|
|
2
|
+
export async function feedbackDeadline(operation, timeoutMs = 20_000) {
|
|
3
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 20_000)
|
|
4
|
+
throw new Error('feedback timeoutMs must be between 1 and 20000');
|
|
5
|
+
let timer;
|
|
6
|
+
try {
|
|
7
|
+
return await Promise.race([
|
|
8
|
+
operation(),
|
|
9
|
+
new Promise((_, reject) => {
|
|
10
|
+
timer = setTimeout(() => reject(new FeedbackDeliveryError('delivery-timeout')), timeoutMs);
|
|
11
|
+
}),
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
if (timer)
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BlackboardClientDependencies } from './client.mts';
|
|
2
|
+
import type { FeedbackDeliveryOptions, FeedbackDeliveryResult, FeedbackDiagnostic, FeedbackOnlineOptions } from './feedback-types.mts';
|
|
3
|
+
export { FeedbackDeliveryError } from './feedback-online-error.mts';
|
|
4
|
+
export declare function writeFeedback(input: FeedbackDeliveryOptions): Promise<FeedbackDeliveryResult>;
|
|
5
|
+
export declare function verifyFreshFeedback(input: FeedbackOnlineOptions): Promise<Extract<FeedbackDeliveryResult, {
|
|
6
|
+
status: 'delivered';
|
|
7
|
+
}>>;
|
|
8
|
+
export declare function flushFeedbackOutbox(input: {
|
|
9
|
+
directory: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
dependencies?: BlackboardClientDependencies;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
status: 'empty' | 'pending';
|
|
14
|
+
pendingCount: number;
|
|
15
|
+
deliveredCount: number;
|
|
16
|
+
diagnostic?: FeedbackDiagnostic;
|
|
17
|
+
cleanupDiagnostic?: 'outbox-cleanup-failed';
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createFeedbackEnvelope, validateFeedbackEnvelope } from './feedback-codec.mjs';
|
|
2
|
+
import { validateFeedbackIdentity } from './feedback-identity.mjs';
|
|
3
|
+
import { deliverFeedbackOnline } from './feedback-online.mjs';
|
|
4
|
+
import { FeedbackDeliveryError, feedbackDiagnostic } from './feedback-online-error.mjs';
|
|
5
|
+
import { feedbackOutboxStatus, listFeedbackOutbox, persistFeedbackOutbox, removeFeedbackOutbox, } from './feedback-outbox.mjs';
|
|
6
|
+
export { FeedbackDeliveryError } from './feedback-online-error.mjs';
|
|
7
|
+
export async function writeFeedback(input) {
|
|
8
|
+
if (input.mode !== 'interactive' && input.mode !== 'autonomous')
|
|
9
|
+
throw new Error('feedback mode must be explicitly interactive or autonomous');
|
|
10
|
+
validateFeedbackIdentity(input.identity);
|
|
11
|
+
validateFeedbackEnvelope(input.envelope);
|
|
12
|
+
const envelope = prepareEnvelope(input);
|
|
13
|
+
if (input.mode === 'autonomous') {
|
|
14
|
+
if (input.outboxDirectory !== undefined)
|
|
15
|
+
throw new Error('autonomous feedback cannot use a filesystem outbox');
|
|
16
|
+
const receipt = await deliverFeedbackOnline({ ...input, envelope });
|
|
17
|
+
return { status: 'delivered', sourceEventId: envelope.sourceEventId, pendingCount: 0, receipt };
|
|
18
|
+
}
|
|
19
|
+
if (!input.outboxDirectory)
|
|
20
|
+
throw new Error('interactive feedback requires an explicit durable outbox directory');
|
|
21
|
+
persistFeedbackOutbox(input.outboxDirectory, { identity: input.identity, envelope });
|
|
22
|
+
try {
|
|
23
|
+
const receipt = await deliverFeedbackOnline({ ...input, envelope });
|
|
24
|
+
const cleanup = removeFeedbackOutbox(input.outboxDirectory, {
|
|
25
|
+
identity: input.identity,
|
|
26
|
+
envelope,
|
|
27
|
+
});
|
|
28
|
+
return { status: 'delivered', sourceEventId: envelope.sourceEventId, ...cleanup, receipt };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const diagnostic = feedbackDiagnostic(error);
|
|
32
|
+
if (['identity-conflict', 'event-conflict', 'archived-session'].includes(diagnostic))
|
|
33
|
+
throw new FeedbackDeliveryError(diagnostic);
|
|
34
|
+
return {
|
|
35
|
+
status: 'pending',
|
|
36
|
+
sourceEventId: envelope.sourceEventId,
|
|
37
|
+
pendingCount: feedbackOutboxStatus(input.outboxDirectory).pendingCount,
|
|
38
|
+
diagnostic,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function verifyFreshFeedback(input) {
|
|
43
|
+
const envelope = prepareEnvelope(input);
|
|
44
|
+
const receipt = await deliverFeedbackOnline({ ...input, envelope }, true);
|
|
45
|
+
return { status: 'delivered', sourceEventId: envelope.sourceEventId, pendingCount: 0, receipt };
|
|
46
|
+
}
|
|
47
|
+
export async function flushFeedbackOutbox(input) {
|
|
48
|
+
let deliveredCount = 0;
|
|
49
|
+
let diagnostic;
|
|
50
|
+
let cleanupDiagnostic;
|
|
51
|
+
for (const record of listFeedbackOutbox(input.directory)) {
|
|
52
|
+
try {
|
|
53
|
+
await deliverFeedbackOnline({
|
|
54
|
+
...record,
|
|
55
|
+
...(input.env === undefined ? {} : { env: input.env }),
|
|
56
|
+
...(input.dependencies === undefined ? {} : { dependencies: input.dependencies }),
|
|
57
|
+
});
|
|
58
|
+
const cleanup = removeFeedbackOutbox(input.directory, record);
|
|
59
|
+
cleanupDiagnostic ??= cleanup.cleanupDiagnostic;
|
|
60
|
+
deliveredCount++;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const current = feedbackDiagnostic(error);
|
|
64
|
+
diagnostic ??= current;
|
|
65
|
+
if (['identity-conflict', 'event-conflict', 'archived-session'].includes(current))
|
|
66
|
+
continue;
|
|
67
|
+
diagnostic = current;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...feedbackOutboxStatus(input.directory),
|
|
73
|
+
deliveredCount,
|
|
74
|
+
...(cleanupDiagnostic === undefined ? {} : { cleanupDiagnostic }),
|
|
75
|
+
...(diagnostic === undefined ? {} : { diagnostic }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function prepareEnvelope(input) {
|
|
79
|
+
const token = (input.env ?? process.env).AGENT_BLACKBOARD_TOKEN;
|
|
80
|
+
const envelope = createFeedbackEnvelope(input.envelope, {
|
|
81
|
+
knownSensitiveValues: token ? [token] : [],
|
|
82
|
+
});
|
|
83
|
+
if (token &&
|
|
84
|
+
token.length >= 4 &&
|
|
85
|
+
JSON.stringify({ identity: input.identity, envelope }).includes(token))
|
|
86
|
+
throw new Error('feedback identity contains a configured sensitive value');
|
|
87
|
+
return envelope;
|
|
88
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FeedbackReference } from './feedback-types.mts';
|
|
2
|
+
export declare function validateFeedbackText(value: unknown, label: string, maximum: number): asserts value is string;
|
|
3
|
+
export declare function normalizeRepositories(value: unknown, required?: boolean): string[];
|
|
4
|
+
export declare function validateFeedbackReferences(value: unknown, label: string): asserts value is FeedbackReference[];
|