@intentic/sandbox-contract 1.222.0 → 1.224.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/dist/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +22 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -1
- package/package.json +4 -4
- package/src/agent-catalog.ts +78 -78
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.ts +67 -67
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +3 -3
- package/src/chores/fix-deps.ts +3 -3
- package/src/chores/probes.ts +16 -16
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.ts +23 -23
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +7 -7
- package/src/events.ts +136 -136
- package/src/history-state.ts +12 -12
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.ts +15 -15
- package/src/index.ts +8 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.ts +30 -30
- package/src/output-fields.ts +9 -9
- package/src/path-refs.ts +6 -6
- package/src/publish-drafts.ts +6 -6
- package/src/quick-model.ts +15 -15
- package/src/routes.ts +19 -19
- package/src/runtime-state.ts +22 -22
- package/src/schemas.ts +930 -918
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.ts +20 -20
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.ts +5 -5
- package/src/workflow-faults.ts +11 -11
- package/src/workspace-state.ts +119 -119
package/src/chores/probes.ts
CHANGED
|
@@ -11,19 +11,19 @@ import {
|
|
|
11
11
|
TAILWIND_PACKAGES,
|
|
12
12
|
} from "./stack.js";
|
|
13
13
|
|
|
14
|
-
/* THE PROBES
|
|
14
|
+
/* THE PROBES, the measurements that cost a subprocess, declared once so the daemon that runs them and the panel
|
|
15
15
|
* that explains them cannot disagree about what "outdated" meant.
|
|
16
16
|
*
|
|
17
17
|
* A spec is a shell command and a parser, deliberately in that order of trust: the command is whatever the tool's
|
|
18
18
|
* own maintainers publish as its machine-readable output, and the parser is written to be DISAPPOINTED. Every one
|
|
19
19
|
* of these tools has changed its JSON shape at least once, they are run against whatever version the repo pinned,
|
|
20
20
|
* and a probe that throws on an unexpected field would take the whole maintenance surface down with it. So each
|
|
21
|
-
* parser walks the structure defensively and returns `undefined` when it cannot recognise what it got
|
|
21
|
+
* parser walks the structure defensively and returns `undefined` when it cannot recognise what it got, which the
|
|
22
22
|
* runner records as a failed probe with the output attached, rather than as a clean repository.
|
|
23
23
|
*
|
|
24
24
|
* TIERS ARE ABOUT COST, and the cost is what sets the cadence. Tier 1 reads metadata that already exists (a
|
|
25
25
|
* lockfile, a registry's version list) and finishes in seconds, so the background runner refreshes it daily. Tier
|
|
26
|
-
* 2 reads the whole tree
|
|
26
|
+
* 2 reads the whole tree, knip type-checks it, jscpd tokenizes every file, and can run for minutes on a large
|
|
27
27
|
* repo, so it refreshes weekly and says how long it took, because a reader deciding whether to force a refresh
|
|
28
28
|
* deserves to know what they are asking for.
|
|
29
29
|
*
|
|
@@ -48,7 +48,7 @@ export interface ProbeSpec {
|
|
|
48
48
|
// Exit 0 ⇒ this repo can be measured. Runs in the repo's own directory, like the command.
|
|
49
49
|
readonly available: string;
|
|
50
50
|
/* What is MISSING when `available` says no, named here rather than derived from the title. The obvious
|
|
51
|
-
* derivation
|
|
51
|
+
* derivation, "this repository has no security advisories to measure", states the one thing an unmeasured
|
|
52
52
|
* probe must never claim, that there are none, and it is the same conflation the block above exists to
|
|
53
53
|
* prevent. Phrased as a bare clause ("no lockfile"), because the panel groups these under its own lead-in. */
|
|
54
54
|
readonly unavailable: string;
|
|
@@ -97,7 +97,7 @@ const semverKind = (current: string, latest: string): OutdatedPackage["kind"] =>
|
|
|
97
97
|
/* `pnpm outdated --json` prints a map of package name → { current, latest, dependencyType }. In a workspace the
|
|
98
98
|
* recursive form merges every package's entries into the same map and adds `dependentPackages`, which is why this
|
|
99
99
|
* reads the map rather than expecting a list: one shape covers both, and a field we don't use appearing is not a
|
|
100
|
-
* parse failure. Entries missing `current` or `latest` are skipped
|
|
100
|
+
* parse failure. Entries missing `current` or `latest` are skipped, that is how pnpm reports a package it could
|
|
101
101
|
* not resolve against the registry, and it is not evidence of anything. */
|
|
102
102
|
const parseOutdated = (stdout: string): ProbeFacts | undefined => {
|
|
103
103
|
const root = asObject(stdout);
|
|
@@ -123,8 +123,8 @@ const parseOutdated = (stdout: string): ProbeFacts | undefined => {
|
|
|
123
123
|
const SEVERITIES = new Set([`critical`, `high`, `moderate`, `low`, `info`]);
|
|
124
124
|
|
|
125
125
|
/* `pnpm audit --json` prints `{ advisories: { <id>: {...} }, metadata: {...} }`. The metadata's counts are
|
|
126
|
-
* deliberately ignored: they are a tally, and this surface needs the advisories themselves
|
|
127
|
-
* whether a patched range exists
|
|
126
|
+
* deliberately ignored: they are a tally, and this surface needs the advisories themselves, which package, and
|
|
127
|
+
* whether a patched range exists, because "there is a fix that is a version bump" and "there is no patch yet"
|
|
128
128
|
* lead to completely different turns, and a count cannot tell them apart.
|
|
129
129
|
*
|
|
130
130
|
* `dev` comes off the findings' own flag rather than being inferred. A build-time-only advisory is real but it is
|
|
@@ -135,7 +135,7 @@ const parseAudit = (stdout: string): ProbeFacts | undefined => {
|
|
|
135
135
|
return undefined;
|
|
136
136
|
}
|
|
137
137
|
const raw = root[`advisories`];
|
|
138
|
-
// No `advisories` key at all is pnpm's clean report
|
|
138
|
+
// No `advisories` key at all is pnpm's clean report, an empty list, not an unrecognisable one.
|
|
139
139
|
if (raw === undefined) {
|
|
140
140
|
return { id: `audit`, advisories: [] };
|
|
141
141
|
}
|
|
@@ -168,7 +168,7 @@ const parseAudit = (stdout: string): ProbeFacts | undefined => {
|
|
|
168
168
|
return { id: `audit`, advisories };
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
/* knip's JSON reporter prints `{ issues: [...] }
|
|
171
|
+
/* knip's JSON reporter prints `{ issues: [...] }`, one row per file that has findings, carrying a per-kind array
|
|
172
172
|
* of what it found there. A wholly unreferenced file is a row whose own `files` array names it, which is why that
|
|
173
173
|
* count is a sum like every other kind rather than a list of its own. Counts plus a sample of the file paths, not
|
|
174
174
|
* the full list: the agent re-runs knip itself against the live tree (a list from a probe hours old would send it
|
|
@@ -233,16 +233,16 @@ const parseJscpd = (stdout: string): ProbeFacts | undefined => {
|
|
|
233
233
|
* Everything it emits is a labelled, tab-separated line, and the first line is always the bare marker `UI`. That
|
|
234
234
|
* marker is the whole reason this parser can tell "the sweep ran and this repository is clean" from "the sweep
|
|
235
235
|
* never ran": every other line is optional, so without it an empty stdout and a spotless codebase are the same
|
|
236
|
-
* string
|
|
236
|
+
* string, and reporting the second when it was the first is the one thing probes.ts exists to prevent. */
|
|
237
237
|
const UI_MARKER = `UI`;
|
|
238
|
-
// Caps, applied after `sort` so truncation is alphabetical and therefore identical between runs
|
|
238
|
+
// Caps, applied after `sort` so truncation is alphabetical and therefore identical between runs, an unsorted
|
|
239
239
|
// truncation would mint a new digest on every sweep and badge forever. What is dropped is genuinely dropped: a
|
|
240
240
|
// component past the cap cannot join a family, and the chore says so rather than implying it saw everything.
|
|
241
241
|
const COMPONENT_LIMIT = 2000;
|
|
242
242
|
const RULE_FILE_LIMIT = 500;
|
|
243
243
|
|
|
244
244
|
/* THE `.` IS LOAD-BEARING, and leaving it off cost this probe every finding it will ever have. Given no path,
|
|
245
|
-
* ripgrep searches the tree only when stdin is a TTY
|
|
245
|
+
* ripgrep searches the tree only when stdin is a TTY, otherwise it reads STDIN, which is exactly how the runner
|
|
246
246
|
* spawns a probe. The sweep therefore ran, exited 0, printed its marker and matched nothing, in every repository,
|
|
247
247
|
* forever: the precise failure the marker line was introduced to make impossible, arriving through the one door it
|
|
248
248
|
* does not cover. It reproduces from Node and not from an interactive shell, which is why it survived being read.
|
|
@@ -367,7 +367,7 @@ const parseBundle = (stdout: string): ProbeFacts | undefined => {
|
|
|
367
367
|
};
|
|
368
368
|
|
|
369
369
|
// Where the tier-2 tools leave their reports. Under /tmp because they are inputs to a parse that happens
|
|
370
|
-
// immediately after, never something to keep
|
|
370
|
+
// immediately after, never something to keep, the cached ProbeResult is the artefact that survives. The same
|
|
371
371
|
// path the scheduled form of this chore uses (chores.ts), so a workspace running both keeps one copy.
|
|
372
372
|
const JSCPD_DIR = `/tmp/intentic-chore-jscpd`;
|
|
373
373
|
|
|
@@ -423,7 +423,7 @@ export const PROBES: readonly ProbeSpec[] = [
|
|
|
423
423
|
timeoutMs: 20 * 60_000,
|
|
424
424
|
available: `test -f package.json`,
|
|
425
425
|
unavailable: `no package.json`,
|
|
426
|
-
// `--threshold 100` so jscpd never fails the command on its own opinion of what is too much duplication
|
|
426
|
+
// `--threshold 100` so jscpd never fails the command on its own opinion of what is too much duplication,
|
|
427
427
|
// that judgement is the chore's, made from the percentage, not the tool's exit code.
|
|
428
428
|
command:
|
|
429
429
|
`pnpm dlx jscpd --reporters json --output ${JSCPD_DIR} --min-lines 12 --threshold 100 . >/dev/null 2>&1; ` +
|
|
@@ -437,11 +437,11 @@ export const PROBES: readonly ProbeSpec[] = [
|
|
|
437
437
|
/* Tier 1 despite reading the whole tree, and the placement is a judgement rather than an oversight. The
|
|
438
438
|
* tier is about COST: this is a dozen ripgrep walks, seconds on a large monorepo, against knip
|
|
439
439
|
* type-checking the tree and jscpd tokenizing every file for minutes. A weekly TTL would also make it the
|
|
440
|
-
* wrong shape
|
|
440
|
+
* wrong shape, its findings move whenever someone writes a component, which is daily. */
|
|
441
441
|
tier: 1,
|
|
442
442
|
ttlMs: DAY_MS,
|
|
443
443
|
timeoutMs: 5 * 60_000,
|
|
444
|
-
// Any manifest in the repo declaring a UI framework or Tailwind, not just the root's
|
|
444
|
+
// Any manifest in the repo declaring a UI framework or Tailwind, not just the root's, a monorepo keeps
|
|
445
445
|
// React in the app package and the root manifest is a handful of build tools.
|
|
446
446
|
available:
|
|
447
447
|
`rg -l --no-messages -g '**/package.json' -g '!**/node_modules/**' ` +
|
package/src/chores/prompt.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/* HOW WE ASK. Every prompt this workspace generates from a measurement
|
|
1
|
+
/* HOW WE ASK. Every prompt this workspace generates from a measurement, a hotspot's refactor, a chore's sweep,
|
|
2
2
|
* has the same four parts, in the same order, for the same reasons:
|
|
3
3
|
*
|
|
4
4
|
* subject the one line that says what is being worked on. First, because a model that reads the rationale
|
|
5
5
|
* before the target starts planning against a subject it has not been told yet.
|
|
6
6
|
* why the NUMBERS, quoted exactly as the panel shows them, then what they mean. Exact so the agent and
|
|
7
7
|
* the person are arguing about one set of facts; the agent can and should recount them.
|
|
8
|
-
* goal what shape to move towards
|
|
8
|
+
* goal what shape to move towards, never a design. Whoever generated this prompt has not read the code,
|
|
9
9
|
* so a prescribed solution from out here is a guess wearing an instruction's clothes.
|
|
10
10
|
* done falsifiable, and checkable by the agent itself. The same resident engine that produced the
|
|
11
11
|
* measurement answers `iq` in the agent's own worktree, so "run it again and see" is available and
|
|
12
12
|
* "I have finished" is not something it has to be taken at its word on.
|
|
13
13
|
*
|
|
14
14
|
* The invariants sit between goal and done because they are the constraints on HOW, and they are stated in full
|
|
15
|
-
* every time rather than assumed. Each one is a specific way the turn fails without it
|
|
15
|
+
* every time rather than assumed. Each one is a specific way the turn fails without it, they are here because
|
|
16
16
|
* they were each learned from a diff nobody could review. */
|
|
17
17
|
|
|
18
18
|
export interface Ask {
|
|
@@ -30,16 +30,16 @@ export const composeAsk = ({ subject, why, diagnosis, goal, invariants, done }:
|
|
|
30
30
|
/* Said to every turn a TOOL woke, and the reason the maintenance surface can point agents at tool output at all.
|
|
31
31
|
* A tool reporting N findings is not reporting N problems: knip is confidently wrong about anything reachable
|
|
32
32
|
* from outside the repo, jscpd counts generated files, an advisory in a build-time dependency is not the same
|
|
33
|
-
* risk as one in a running service. A chore that mechanically actions the whole list is worse than no chore
|
|
33
|
+
* risk as one in a running service. A chore that mechanically actions the whole list is worse than no chore,
|
|
34
34
|
* it makes noisy, confident, wrong changes at three in the morning, and the next person has to review a diff
|
|
35
35
|
* whose author had no opinion about it. */
|
|
36
36
|
export const TRIAGE_NOTE =
|
|
37
37
|
`The measurement woke you; it did not decide anything. Read the repository before you touch it, and treat every ` +
|
|
38
38
|
`finding as a claim to verify rather than a task to execute. If a finding is wrong, say why in one line and leave ` +
|
|
39
|
-
`it
|
|
39
|
+
`it. A run that verifies ten and fixes two is a good run.`;
|
|
40
40
|
|
|
41
41
|
/* The invariants for a turn that CHANGES things. Whatever it does lands as uncommitted work in the owner's
|
|
42
|
-
* workspace, so it is reviewed as one diff by someone who did not watch it happen
|
|
42
|
+
* workspace, so it is reviewed as one diff by someone who did not watch it happen, which is what every clause
|
|
43
43
|
* here is protecting.
|
|
44
44
|
*
|
|
45
45
|
* "Separately explainable" is doing the most work: a chore that fixes its findings AND tidies what it passed on
|
|
@@ -57,8 +57,8 @@ export const REPORT_INVARIANTS =
|
|
|
57
57
|
`can act on or dismiss. Where you would propose an edit, describe it and where it would go instead of making it.`;
|
|
58
58
|
|
|
59
59
|
/* The invariants for a turn refactoring ONE FILE, as the codebase-health panel's rows ask for. Distinct from the
|
|
60
|
-
* chore ones because the
|
|
60
|
+
* chore ones because the scope of changes is the thing at stake: named as a scope rather than "only this file",
|
|
61
61
|
* since half those archetypes ask for new files and must not read as forbidding them. */
|
|
62
62
|
export const REFACTOR_INVARIANTS =
|
|
63
|
-
`Read it first. Behaviour stays identical
|
|
64
|
-
`importers that must follow
|
|
63
|
+
`Read it first. Behaviour stays identical. Changes affect only this file, whatever it splits into, and the ` +
|
|
64
|
+
`importers that must follow. Leave no re-export shims behind.`;
|
package/src/chores/stack.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* WHAT THIS REPOSITORY IS BUILT WITH, and the patterns that follow from it
|
|
1
|
+
/* WHAT THIS REPOSITORY IS BUILT WITH, and the patterns that follow from it, the table the UI chores are written
|
|
2
2
|
* against, kept apart from both the probe that runs it and the chores that read it.
|
|
3
3
|
*
|
|
4
4
|
* It sits in the middle on purpose. probes.ts composes ONE ripgrep sweep out of the rules below, and chores.ts
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* THE PATTERNS ARE RIPGREP'S DIALECT, and they carry two constraints that are not obvious from reading them:
|
|
14
14
|
*
|
|
15
15
|
* No literal apostrophe, ever. The scan command wraps each pattern in shell single quotes, so a `'` inside one
|
|
16
|
-
* would end the quoting and hand the rest of the regex to the shell. Match quotes as `[\x22\x27]` instead
|
|
16
|
+
* would end the quoting and hand the rest of the regex to the shell. Match quotes as `[\x22\x27]` instead,
|
|
17
17
|
* Rust's regex crate reads those escapes, and the shell never sees a quote character at all. stack.test.ts
|
|
18
18
|
* enforces this, because the failure is a probe that dies at three in the morning in someone else's workspace
|
|
19
19
|
* rather than anything a reader would notice here.
|
|
20
20
|
*
|
|
21
21
|
* No lookaround. Rust's regex crate has none, and reaching for ripgrep's PCRE2 mode to get it would make the
|
|
22
22
|
* sweep depend on how the box's ripgrep was compiled. A rule that seems to need it is usually asking a question
|
|
23
|
-
* about the FILE rather than about a line
|
|
23
|
+
* about the FILE rather than about a line, see `absent` below, which is what that question actually is. */
|
|
24
24
|
|
|
25
25
|
export interface UiFramework {
|
|
26
26
|
readonly id: string;
|
|
@@ -38,7 +38,7 @@ export const UI_FRAMEWORKS: readonly UiFramework[] = [
|
|
|
38
38
|
{ id: `angular`, label: `Angular`, packages: [`@angular/core`] },
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
-
// Tailwind is not in the table above because it is not a UI framework and does not own any idiom rules
|
|
41
|
+
// Tailwind is not in the table above because it is not a UI framework and does not own any idiom rules, it is a
|
|
42
42
|
// styling system that any of the three can be wearing, and it gates exactly one chore.
|
|
43
43
|
export const TAILWIND_PACKAGES: readonly string[] = [`tailwindcss`];
|
|
44
44
|
|
|
@@ -64,20 +64,20 @@ export const SCAN_IGNORES: readonly string[] = [
|
|
|
64
64
|
`!**/*.{test,spec,stories}.*`,
|
|
65
65
|
];
|
|
66
66
|
|
|
67
|
-
// What counts as a component file, across all three frameworks at once. The sweep cannot vary by repository
|
|
68
|
-
// probe's command is a fixed string
|
|
67
|
+
// What counts as a component file, across all three frameworks at once. The sweep cannot vary by repository, a
|
|
68
|
+
// probe's command is a fixed string, so it asks for all of them and a Vue-only repo simply has no `.tsx` files.
|
|
69
69
|
export const COMPONENT_GLOBS: readonly string[] = [`*.vue`, `*.tsx`, `*.jsx`, `*.component.ts`];
|
|
70
70
|
|
|
71
71
|
// Where a Tailwind class can appear. Wider than COMPONENT_GLOBS because a class list lives in markup as often as
|
|
72
|
-
// in a component
|
|
72
|
+
// in a component, an Angular template and a plain .html page both style with the same utilities.
|
|
73
73
|
export const MARKUP_GLOBS: readonly string[] = [`*.vue`, `*.tsx`, `*.jsx`, `*.html`, `*.svelte`, `*.astro`];
|
|
74
74
|
|
|
75
|
-
/* THE DESIGN SYSTEM BYPASS. Not "any arbitrary value"
|
|
75
|
+
/* THE DESIGN SYSTEM BYPASS. Not "any arbitrary value", `grid-cols-[1fr_auto]` and `w-[calc(100%-2rem)]` are
|
|
76
76
|
* Tailwind working as designed, and a chore that counted them would be objecting to the feature rather than to
|
|
77
77
|
* anything wrong. What this matches is the two arbitrary values that route around a decision the theme already
|
|
78
78
|
* made: a colour that is not in the palette, and a pixel size that is not on the spacing or type scale.
|
|
79
79
|
*
|
|
80
|
-
* The leading `-` is
|
|
80
|
+
* The leading `-` is required. It anchors the match to a utility prefix (`bg-`, `text-`, `w-`), so a bare
|
|
81
81
|
* `[...]` in ordinary prose or an array index cannot be mistaken for a class. */
|
|
82
82
|
export const BYPASS_PATTERN = `-\\[(#[0-9a-fA-F]{3,8}|(rgb|hsl)a?\\(|[0-9]+(\\.[0-9]+)?px)`;
|
|
83
83
|
|
|
@@ -87,12 +87,12 @@ export interface IdiomRule {
|
|
|
87
87
|
readonly framework: string;
|
|
88
88
|
// What the repository still has, named as the reader would name it.
|
|
89
89
|
readonly label: string;
|
|
90
|
-
// What replaced it. Carried so the prompt can say where to go rather than only what to leave
|
|
90
|
+
// What replaced it. Carried so the prompt can say where to go rather than only what to leave, an agent told
|
|
91
91
|
// "you still use NgModule" and nothing else will pick a destination, and it may not pick this one.
|
|
92
92
|
readonly replacement: string;
|
|
93
93
|
readonly pattern: string;
|
|
94
94
|
readonly globs: readonly string[];
|
|
95
|
-
/* THE IDIOM IS THE PATTERN BEING MISSING, not present
|
|
95
|
+
/* THE IDIOM IS THE PATTERN BEING MISSING, not present, `pattern` names the NEW way, and the file is on the
|
|
96
96
|
* old one precisely because the new one does not appear in it anywhere. The sweep spells this
|
|
97
97
|
* `--files-without-match`.
|
|
98
98
|
*
|
|
@@ -100,7 +100,7 @@ export interface IdiomRule {
|
|
|
100
100
|
* <script setup>" was first written as a lookahead over `<script`, which matches per LINE: a migrated
|
|
101
101
|
* component with a second plain `<script>` block for defineOptions, or one that merely mentions `<script` in
|
|
102
102
|
* a comment, both read as un-migrated. It reported five files in an application whose 167 SFCs are every one
|
|
103
|
-
* of them migrated. The question was never "is there a line like this"
|
|
103
|
+
* of them migrated. The question was never "is there a line like this", it is "does this file contain the
|
|
104
104
|
* new idiom at all", which is one flag rather than a cleverer regex, and it costs no PCRE2.
|
|
105
105
|
*
|
|
106
106
|
* The globs carry more weight on an absent rule than on a normal one, and narrowly is the only safe way to
|
|
@@ -110,7 +110,7 @@ export interface IdiomRule {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/* THE IDIOMS THEIR OWN MAINTAINERS HAVE MOVED ON FROM. Every rule here names something the framework's own
|
|
113
|
-
* documentation now steers people away from, and every one of them still works
|
|
113
|
+
* documentation now steers people away from, and every one of them still works, which is exactly why they
|
|
114
114
|
* accumulate, and why no editor and no linter will bring them up unprompted.
|
|
115
115
|
*
|
|
116
116
|
* High confidence over coverage. Each pattern is one a reader can check by eye against a file, and the ones that
|
|
@@ -156,7 +156,7 @@ export const IDIOM_RULES: readonly IdiomRule[] = [
|
|
|
156
156
|
framework: `vue`,
|
|
157
157
|
label: `the Options API`,
|
|
158
158
|
replacement: `<script setup> with the Composition API`,
|
|
159
|
-
// The new idiom, inverted by `absent` below
|
|
159
|
+
// The new idiom, inverted by `absent` below, an SFC that never opens a `<script setup>` tag is still on
|
|
160
160
|
// the old one. A file with no script block at all is swept up too, and that is the honest reading: it has
|
|
161
161
|
// not been migrated because there was nothing there to migrate.
|
|
162
162
|
pattern: `<script[^>]*\\bsetup\\b`,
|
|
@@ -213,14 +213,14 @@ export const idiomRule = (id: string): IdiomRule | undefined => IDIOM_RULES.find
|
|
|
213
213
|
* side had two extra characters. */
|
|
214
214
|
export const normalizePath = (path: string): string => path.replace(/^\.\//, ``);
|
|
215
215
|
|
|
216
|
-
// Below this a stem is too short to have survived the stripping above with its meaning intact
|
|
216
|
+
// Below this a stem is too short to have survived the stripping above with its meaning intact, `H1` and `H2`
|
|
217
217
|
// would both reduce to `h` and read as one family of heading components that are not duplicates of anything.
|
|
218
218
|
const MIN_STEM = 3;
|
|
219
219
|
|
|
220
220
|
const QUALIFIER_PREFIX = /^(base|the)/;
|
|
221
221
|
const QUALIFIER_SUFFIX = /(v[0-9]+|new|old|legacy|copy|component|[0-9]+)$/;
|
|
222
222
|
|
|
223
|
-
/* THE NAME TWO COMPONENTS SHARE WHEN THEY ARE THE SAME COMPONENT TWICE
|
|
223
|
+
/* THE NAME TWO COMPONENTS SHARE WHEN THEY ARE THE SAME COMPONENT TWICE, or `undefined` when the file has no
|
|
224
224
|
* name worth comparing.
|
|
225
225
|
*
|
|
226
226
|
* This is a normaliser, not a similarity score, and that is the point: it answers a question the reader can check
|
|
@@ -232,7 +232,7 @@ const QUALIFIER_SUFFIX = /(v[0-9]+|new|old|legacy|copy|component|[0-9]+)$/;
|
|
|
232
232
|
* forty index files is a finding about the naming convention rather than about any duplication. */
|
|
233
233
|
export const componentStem = (path: string): string | undefined => {
|
|
234
234
|
const file = normalizePath(path).split(`/`).pop() ?? ``;
|
|
235
|
-
// `.component.ts` loses both suffixes, `.vue` loses one
|
|
235
|
+
// `.component.ts` loses both suffixes, `.vue` loses one, taking everything before the first dot handles both
|
|
236
236
|
// without a table, since a component's name is never the part after a dot.
|
|
237
237
|
const base = (file.split(`.`)[0] ?? ``).toLowerCase().replace(/[^a-z0-9]/g, ``);
|
|
238
238
|
if (base === `` || base === `index`) {
|
package/src/chores/verdict.ts
CHANGED
|
@@ -2,18 +2,18 @@ import type { ChoreLedgerEntry, ChoresReport, ProbeId, ProbeResult } from "../sc
|
|
|
2
2
|
import { type Chore, type ChoreContext, type ChoreFinding, CHORES, chorePrompt } from "./chores.js";
|
|
3
3
|
import { probeSpec } from "./probes.js";
|
|
4
4
|
|
|
5
|
-
/* FROM EVIDENCE TO A VERDICT
|
|
5
|
+
/* FROM EVIDENCE TO A VERDICT, the one place that decides whether a chore is due, and the only place that is
|
|
6
6
|
* allowed to. Both the Maintenance panel and its rail badge run this function over the same report, so the number
|
|
7
7
|
* on the tile and the reason in the panel are the same computation and cannot drift apart.
|
|
8
8
|
*
|
|
9
9
|
* Six states, and the distinctions between them are the whole design:
|
|
10
10
|
*
|
|
11
|
-
* not-applicable this chore is not a QUESTION worth asking of this repository
|
|
11
|
+
* not-applicable this chore is not a QUESTION worth asking of this repository, there is no Dockerfile to
|
|
12
12
|
* slim, no pipeline to tighten, no documentation to re-read. Dropped from the panel entirely
|
|
13
13
|
* rather than shown as clear, because "clear" claims we checked, and there was nothing to check.
|
|
14
14
|
* The reason survives in the scope strip, so "why is there no Docker chore here?" has an answer.
|
|
15
15
|
* unavailable we have not measured this. knip is not a devDependency; there is no lockfile to audit. Rendered
|
|
16
|
-
* greyed, never badged, and never collapsed into `clear
|
|
16
|
+
* greyed, never badged, and never collapsed into `clear`, a maintenance surface reporting a green
|
|
17
17
|
* repository it has never actually measured is worse than one that says nothing.
|
|
18
18
|
* clear we measured, and there is nothing to do. This is the common state, and it has to be visibly
|
|
19
19
|
* reachable or the panel is just a list of complaints.
|
|
@@ -23,23 +23,23 @@ import { probeSpec } from "./probes.js";
|
|
|
23
23
|
* due there is something to do.
|
|
24
24
|
*
|
|
25
25
|
* The first three are all ways of saying "no", and keeping them apart is what makes the surface trustworthy: they
|
|
26
|
-
* mean we cannot ask, we did not measure, and we measured and found nothing
|
|
26
|
+
* mean we cannot ask, we did not measure, and we measured and found nothing, three different claims, and only
|
|
27
27
|
* the last one is reassurance.
|
|
28
28
|
*
|
|
29
29
|
* And one flag that is not a state: `settled`. A due chore that has been RE-MEASURED since a turn was spent on
|
|
30
|
-
* it, and whose evidence did not move, stays due
|
|
30
|
+
* it, and whose evidence did not move, stays due, because it is, but must never light the rail again. This is
|
|
31
31
|
* what stops the surface repeating itself while a fix sits in review, and it is why the ledger stores a digest
|
|
32
32
|
* rather than a timestamp: "ran 3 days ago" cannot tell you whether it ran against THIS.
|
|
33
33
|
*
|
|
34
34
|
* `stale` is the other half of that sentence, and it exists because the digest alone cannot tell the two apart.
|
|
35
35
|
* A probe that never re-ran produces an unchanged digest for free, so "the fix did not move the numbers" and "we
|
|
36
|
-
* have not looked since the fix" arrived at this function looking identical
|
|
36
|
+
* have not looked since the fix" arrived at this function looking identical, and the panel showed the second as
|
|
37
37
|
* the first, quoting a week-old count an hour after the work that invalidated it. Comparing the run's time to the
|
|
38
38
|
* MEASUREMENT's time is what separates them, and it is a comparison of two numbers the report already carries.
|
|
39
39
|
*
|
|
40
40
|
* Nothing here can hide a problem. Snoozing and settling change whether the rail SPEAKS; the panel still shows
|
|
41
41
|
* the chore, its evidence and its state. The one thing that removes a row entirely is `not-applicable`, and that
|
|
42
|
-
* is not hiding
|
|
42
|
+
* is not hiding, it is the absence of a subject, counted in the panel's scope strip and expandable to the reason.
|
|
43
43
|
* A maintenance surface you can quietly bury findings in is a maintenance surface nobody trusts. */
|
|
44
44
|
|
|
45
45
|
export type ChoreState = "due" | "clear" | "snoozed" | "stale" | "unavailable" | "not-applicable";
|
|
@@ -49,16 +49,16 @@ export interface ChoreVerdict {
|
|
|
49
49
|
readonly repo: string;
|
|
50
50
|
readonly state: ChoreState;
|
|
51
51
|
readonly severity: ChoreFinding["severity"];
|
|
52
|
-
// Always present, in every state
|
|
52
|
+
// Always present, in every state, "nothing to do" and "not measured" are answers a reader deserves in words.
|
|
53
53
|
readonly headline: string;
|
|
54
54
|
readonly detail: readonly string[];
|
|
55
55
|
// The evidence identity. Empty for `unavailable`, where there is no evidence to identify.
|
|
56
56
|
readonly digest: string;
|
|
57
|
-
/* WHEN THE EVIDENCE WAS TAKEN
|
|
57
|
+
/* WHEN THE EVIDENCE WAS TAKEN, the fact every row shows beside its numbers, and the one whose absence let a
|
|
58
58
|
* measurement from last Tuesday read as this morning's. Undefined when the verdict rests on no measurement at
|
|
59
59
|
* all: a survey is decided by the calendar, and an unavailable chore has nothing to be out of date. */
|
|
60
60
|
readonly measuredAt: number | undefined;
|
|
61
|
-
// The turn. Present only when there is something to do
|
|
61
|
+
// The turn. Present only when there is something to do, a "start an agent" button on a clear chore is an
|
|
62
62
|
// invitation to spend money proving that nothing is wrong.
|
|
63
63
|
readonly prompt: string | undefined;
|
|
64
64
|
readonly lastRun: ChoreLedgerEntry | undefined;
|
|
@@ -68,7 +68,7 @@ export interface ChoreVerdict {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/* HOW OLD THE EVIDENCE IS: the OLDEST of the measurements a verdict rests on, because a claim is only as current
|
|
71
|
-
* as the least current thing it was computed from. Undefined when it rests on none
|
|
71
|
+
* as the least current thing it was computed from. Undefined when it rests on none, a survey has no measurement,
|
|
72
72
|
* and an unavailable chore's probe did not produce one. */
|
|
73
73
|
const measurementAge = (needs: readonly ProbeId[], probes: ReadonlyMap<ProbeId, ProbeResult>): number | undefined => {
|
|
74
74
|
const taken = needs.flatMap((id) => {
|
|
@@ -79,7 +79,7 @@ const measurementAge = (needs: readonly ProbeId[], probes: ReadonlyMap<ProbeId,
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
// A survey that is clear is clear because it was READ recently, and saying so is the only way its row means
|
|
82
|
-
// anything
|
|
82
|
+
// anything, "nothing to do" under a chore that has no measurement would be a claim about the code rather than
|
|
83
83
|
// about the calendar.
|
|
84
84
|
const clearHeadline = (chore: Chore, lastRun: ChoreLedgerEntry | undefined, nowMs: number): string =>
|
|
85
85
|
chore.survey === true && lastRun !== undefined ? `Surveyed ${Math.round((nowMs - lastRun.ranAt) / 86_400_000)} days ago` : `Nothing to do`;
|
|
@@ -109,9 +109,9 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
|
|
|
109
109
|
const base = { chore, repo: context.repo, lastRun: ledger, settled: false, prompt: undefined } as const;
|
|
110
110
|
|
|
111
111
|
/* APPLICABILITY FIRST, before anything is measured or any evidence is read. A chore that does not apply is
|
|
112
|
-
* not "clear" and not "unmeasured"
|
|
113
|
-
* function would be answering it anyway. The cause is carried as the headline, because the scope strip
|
|
114
|
-
* groups these rows BY it
|
|
112
|
+
* not "clear" and not "unmeasured", the question does not arise here, and every subsequent branch of this
|
|
113
|
+
* function would be answering it anyway. The cause is carried as the headline, because the scope strip, which
|
|
114
|
+
* groups these rows BY it, is the only place it will ever be read. */
|
|
115
115
|
const inapplicable = chore.applies?.(context.signals);
|
|
116
116
|
if (inapplicable !== undefined) {
|
|
117
117
|
return { ...base, state: `not-applicable`, severity: `info`, headline: inapplicable, detail: [], digest: ``, measuredAt: undefined };
|
|
@@ -138,7 +138,7 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/* Has the last run's settlement lapsed? A cadence of 0 means "this is decided by evidence alone"
|
|
141
|
+
/* Has the last run's settlement lapsed? A cadence of 0 means "this is decided by evidence alone", an advisory
|
|
142
142
|
* does not become worth looking at again because ninety days passed, it becomes worth looking at again when
|
|
143
143
|
* the advisory set changes. Anything with a cadence expires its own settlement, so "we looked and chose not to
|
|
144
144
|
* act" cannot silence a chore for good. */
|
|
@@ -176,7 +176,7 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/* The agent looked at exactly this evidence and reported that there was nothing in it
|
|
179
|
+
/* The agent looked at exactly this evidence and reported that there was nothing in it, knip's findings were
|
|
180
180
|
* all public entry points, the clones were all generated files. That verdict has to stick, or the next poll
|
|
181
181
|
* starts the same turn again and the surface has taught the owner that its rows are wrong. It stops sticking
|
|
182
182
|
* when the evidence changes (a different digest) or the cadence lapses. */
|
|
@@ -193,10 +193,10 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/* THE MEASUREMENT IS OLDER THAN THE WORK. A turn landed after the last time we looked, so the evidence below
|
|
196
|
-
* describes a tree that no longer exists
|
|
196
|
+
* describes a tree that no longer exists, an hour after a run deleted the dead code, the row was still
|
|
197
197
|
* quoting the count from six days before it. `sameEvidence` cannot catch this: an unchanged digest is exactly
|
|
198
198
|
* what a probe that never re-ran produces, so the flag says "settled" at its most confident when it knows
|
|
199
|
-
* least. The chore steps down instead
|
|
199
|
+
* least. The chore steps down instead, evidence stays on the row, the CLAIM comes off it, and carries no
|
|
200
200
|
* prompt, because the honest next move is to measure again rather than to spend a second turn on a finding
|
|
201
201
|
* nobody has re-checked. It cannot badge either, which is what stops the tile lighting for work already done. */
|
|
202
202
|
if (ledger !== undefined && measuredAt !== undefined && ledger.ranAt > measuredAt) {
|
|
@@ -220,8 +220,8 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
|
|
|
220
220
|
// two independent questions with two independent answers.
|
|
221
221
|
export const ledgerKey = (repo: string, chore: string): string => `${repo}|${chore}`;
|
|
222
222
|
|
|
223
|
-
/* Every chore in every repo, from one report. This is what both surfaces call
|
|
224
|
-
* badge filters it
|
|
223
|
+
/* Every chore in every repo, from one report. This is what both surfaces call, the panel groups the result, the
|
|
224
|
+
* badge filters it, so there is exactly one traversal of the book in the codebase and adding a chore to CHORES
|
|
225
225
|
* reaches both surfaces without touching either. */
|
|
226
226
|
export const assessReport = (report: ChoresReport, nowMs: number): ChoreVerdict[] => {
|
|
227
227
|
const ledger = new Map(report.ledger.map((entry) => [ledgerKey(entry.repo, entry.chore), entry]));
|
|
@@ -232,10 +232,10 @@ export const assessReport = (report: ChoresReport, nowMs: number): ChoreVerdict[
|
|
|
232
232
|
};
|
|
233
233
|
|
|
234
234
|
/* WHAT THE RAIL IS ALLOWED TO SAY. A badge must mean "something happened here that you don't already know about",
|
|
235
|
-
* never "here is a statistic"
|
|
235
|
+
* never "here is a statistic", the extension API states that bar and this is the function that holds this
|
|
236
236
|
* surface to it. Three filters, and every one of them removes a case that would otherwise light the tile forever:
|
|
237
237
|
*
|
|
238
|
-
* state === due the obvious one
|
|
238
|
+
* state === due the obvious one, and it is also what keeps `stale` silent, since a measurement taken before
|
|
239
239
|
* the last turn is not a fact anyone should be interrupted about.
|
|
240
240
|
* !settled a turn has been spent on this chore and the re-measured evidence did not move.
|
|
241
241
|
* unseen digest the owner has already LOOKED at this evidence in the panel. Acknowledgement is per digest
|
package/src/contract-lock.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
/* THE WIRE SURFACE, AS ONE COMPARABLE DOCUMENT
|
|
1
|
+
/* THE WIRE SURFACE, AS ONE COMPARABLE DOCUMENT, every zod schema this package exports, serialized to JSON
|
|
2
2
|
* Schema and sorted into a canonical shape. `contract.lock.json` at the package root is this function's
|
|
3
3
|
* committed output, and the pair is what makes "did the contract change?" a question git can answer.
|
|
4
4
|
*
|
|
5
5
|
* Two readers depend on that answer. contract-lock.test.ts fails whenever the committed file is out of step
|
|
6
|
-
* with the code, so a contract change always lands WITH its lock diff
|
|
6
|
+
* with the code, so a contract change always lands WITH its lock diff, visible in review, attributable to a
|
|
7
7
|
* commit. And prepass.mjs (invariant 6) compares the committed lock against its merge-base to insist that a
|
|
8
|
-
* SHRUNK surface
|
|
8
|
+
* SHRUNK surface, a schema or field removed, a type changed, arrives as a `!` commit carrying a
|
|
9
9
|
* `Breaking-Note:` trailer, which is what feeds the release's "Breaking changes" section and the update card's
|
|
10
10
|
* warning. Additions pass freely; every persisted-manifest reader parses loosely, so growth breaks nobody.
|
|
11
11
|
*
|
|
12
12
|
* Serialized from the package's EXPORTS rather than a hand-kept list, on the repo's own rule (AGENTS.md:
|
|
13
13
|
* "guard invariants by discovery, not enumeration"): a schema added tomorrow is in the lock tomorrow, and a
|
|
14
14
|
* schema that stops being exported is a removal the lock shows. `unrepresentable: "any"` keeps the rare
|
|
15
|
-
* function-valued corner from throwing
|
|
15
|
+
* function-valued corner from throwing, it serializes as `{}`, which still diffs when it moves. */
|
|
16
16
|
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import * as contract from "./index.js";
|
|
19
19
|
|
|
20
20
|
// Canonical ordering, so two runs of the same code are byte-identical and a lock diff is a contract diff
|
|
21
|
-
// rather than a key-order shuffle. Arrays keep their order
|
|
21
|
+
// rather than a key-order shuffle. Arrays keep their order, for `required` and `enum` lists the order zod
|
|
22
22
|
// emits is stable, and sorting them would hide a reorder that is genuinely no change at all anyway.
|
|
23
23
|
const sorted = (value: unknown): unknown => {
|
|
24
24
|
if (Array.isArray(value)) {
|
|
@@ -43,7 +43,7 @@ export const currentLock = (): Record<string, unknown> => {
|
|
|
43
43
|
}
|
|
44
44
|
try {
|
|
45
45
|
const schema = sorted(z.toJSONSchema(value, { unrepresentable: "any" })) as Record<string, unknown>;
|
|
46
|
-
// The dialect banner is identical on all ~500 entries
|
|
46
|
+
// The dialect banner is identical on all ~500 entries, pure weight, no surface.
|
|
47
47
|
delete schema["$schema"];
|
|
48
48
|
lock[name] = schema;
|
|
49
49
|
} catch {
|
|
@@ -55,7 +55,7 @@ export const currentLock = (): Record<string, unknown> => {
|
|
|
55
55
|
return lock;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
/* ONE LINE PER SCHEMA, on purpose
|
|
58
|
+
/* ONE LINE PER SCHEMA, on purpose, not JSON.stringify(lock, null, 4). Pretty-printed, the lock is a 35k-line
|
|
59
59
|
* wall nobody scrolls; a line per export keeps it ~500 lines and makes `git diff` read as the list of WHICH
|
|
60
60
|
* surfaces moved, which is the level a reviewer reviews at. The path-level detail lives in the tooling: the
|
|
61
61
|
* lock test diffs parsed objects and prepass invariant 6 names the exact removed paths, so nothing is lost by
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
2
|
import { ActivityListSchema, ActivityQuerySchema, ActivityStatusSchema } from "../schemas.js";
|
|
3
3
|
|
|
4
|
-
// The activity audit feed (provider-agnostic; Discord is the first source). Read-only by design
|
|
4
|
+
// The activity audit feed (provider-agnostic; Discord is the first source). Read-only by design,
|
|
5
5
|
// events are appended daemon-side only, so the log stays a trustworthy record of what the agent did.
|
|
6
6
|
export const activityContract = {
|
|
7
7
|
list: oc.route({ method: "GET", path: "/activity" }).input(ActivityQuerySchema).output(ActivityListSchema),
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "../schemas.js";
|
|
15
15
|
|
|
16
16
|
// A turn EXECUTES as a detached daemon-side run: `run` starts it and acks with the run id; any number of
|
|
17
|
-
// clients render it via `attach` (replay from a seq cursor, then live)
|
|
17
|
+
// clients render it via `attach` (replay from a seq cursor, then live), the initiating window holds no
|
|
18
18
|
// special stream, so a reload, a second window, or another device attaches identically. `reply` un-parks a
|
|
19
19
|
// turn waiting on any interactive card (plan approval, clarifying questions, a per-tool permission prompt);
|
|
20
20
|
// steer injects a user message into the running turn; stop hard-cancels it daemon-side.
|
|
@@ -25,7 +25,7 @@ export const agentContract = {
|
|
|
25
25
|
steer: oc.route({ method: "POST", path: "/agent/steer" }).input(SteerSchema).output(OkSchema),
|
|
26
26
|
stop: oc.route({ method: "POST", path: "/agent/stop" }).input(StopTurnSchema).output(OkSchema),
|
|
27
27
|
// Go back to a message: restore the workspace to that turn's checkpoint, drop the messages after it, and
|
|
28
|
-
// forget the provider session. CONFLICT while a turn is running
|
|
28
|
+
// forget the provider session. CONFLICT while a turn is running, a restore cannot overwrite files an
|
|
29
29
|
// agent is editing. NOT_FOUND when that message has no checkpoint to go back to.
|
|
30
30
|
rewind: oc.route({ method: "POST", path: "/agent/rewind" }).input(RewindTurnSchema).output(RewindResultSchema),
|
|
31
31
|
// The provider's slash commands as last published by one of its turns, so a conversation's `/` popover is
|
|
@@ -24,14 +24,14 @@ import {
|
|
|
24
24
|
|
|
25
25
|
// The fleet: every registered conversation-agent (see AgentSummarySchema). Registry-level actions (read,
|
|
26
26
|
// rename, seen, archive) apply uniformly. For branch-backed conversations, `diff` is the worktree's CUMULATIVE
|
|
27
|
-
// multi-repo delta vs its recorded per-repo bases
|
|
27
|
+
// multi-repo delta vs its recorded per-repo bases, one flat change set per repo
|
|
28
28
|
// (AgentChanges), each file flagged `landed` or not, deliberately not the working tree's staged/unstaged
|
|
29
29
|
// shape: a worktree the user never checks out has no index they could stage into. `land` merges the worktree
|
|
30
30
|
// branches into the main tree (per-repo, conflicts reported, nothing lost on failure); `discard` removes
|
|
31
31
|
// worktrees + branches + registry entry. Those branch actions reject workspace conversations explicitly. An
|
|
32
32
|
// unknown {id} is NOT_FOUND; land/discard while the turn runs is CONFLICT.
|
|
33
|
-
// `rename` sets the user-chosen display title
|
|
34
|
-
// `seen`/`seenAll` stamp the read marker behind the cards' unread badge (AgentSummarySchema.seenAt)
|
|
33
|
+
// `rename` sets the user-chosen display title, legal mid-turn (it touches no worktree state).
|
|
34
|
+
// `seen`/`seenAll` stamp the read marker behind the cards' unread badge (AgentSummarySchema.seenAt), also
|
|
35
35
|
// legal mid-turn, and like `rename` they never bump `updatedAt` (reading is not activity).
|
|
36
36
|
//
|
|
37
37
|
// ARCHIVE is the non-destructive counterpart to discard, and the one the board leans on: for an isolated
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
// surviving branch. Archiving a running agent is CONFLICT, same as land/discard.
|
|
42
42
|
//
|
|
43
43
|
// PURGE empties the archive, and it is `discard` applied to every agent already in there: worktree remnants,
|
|
44
|
-
// branches and entries all go. Deliberately the whole archive and not a list of ids
|
|
44
|
+
// branches and entries all go. Deliberately the whole archive and not a list of ids, the archive is the pile
|
|
45
45
|
// of agents the user has already decided are over, so "clean it up" is one act with one confirmation, and a
|
|
46
46
|
// per-id purge would be `discard`, which already exists. Never touches a running agent (a turn un-archives its
|
|
47
47
|
// own agent, so there should be none) and answers with what it actually deleted: a teardown that fails on one
|
|
@@ -49,31 +49,31 @@ import {
|
|
|
49
49
|
export const agentsContract = {
|
|
50
50
|
list: oc.route({ method: "GET", path: "/agents" }).output(AgentsListSchema),
|
|
51
51
|
archived: oc.route({ method: "GET", path: "/agents/archived" }).output(AgentsListSchema),
|
|
52
|
-
// The board's filter. Answers over BOTH halves of the fleet
|
|
52
|
+
// The board's filter. Answers over BOTH halves of the fleet, the live roster and the archive, because
|
|
53
53
|
// the board hides by design (the Finished lane windows to a handful, archived agents are off the roster
|
|
54
54
|
// entirely), and a filter that reports "no matches" while the agent sits one click away is a lie. The
|
|
55
55
|
// never-carded conversations that are neither are `sessions.list`'s query, which matches by the same rule.
|
|
56
56
|
search: oc.route({ method: "GET", path: "/agents/search" }).input(AgentSearchQuerySchema).output(AgentSearchResultSchema),
|
|
57
57
|
get: oc.route({ method: "GET", path: "/agents/{id}" }).input(AgentIdSchema).output(AgentSummarySchema),
|
|
58
58
|
transcript: oc.route({ method: "GET", path: "/agents/{id}/transcript" }).input(AgentIdSchema).output(AgentTranscriptSchema),
|
|
59
|
-
/* SPEAK AS THE AGENT
|
|
59
|
+
/* SPEAK AS THE AGENT, append the user's words to the conversation's record as an assistant row, with no
|
|
60
60
|
* turn behind them and no reply. The row is marked `placed` for human readers (RestoredMessageSchema); the
|
|
61
61
|
* provider session is FORGOTTEN in the same breath, rewind-style, so the next real turn opens a fresh
|
|
62
|
-
* runtime session seeded from the record
|
|
62
|
+
* runtime session seeded from the record, where the placed line reads as the agent's own words, because
|
|
63
63
|
* the handoff renders every assistant row identically. A running turn is CONFLICT: the illusion can only be
|
|
64
64
|
* established between turns, and a concurrent turn would resume the very session this exists to retire. */
|
|
65
65
|
place: oc.route({ method: "POST", path: "/agents/{id}/place" }).input(AgentPlaceSchema).output(OkSchema),
|
|
66
66
|
rename: oc.route({ method: "POST", path: "/agents/{id}/rename" }).input(AgentRenameSchema).output(AgentSummarySchema),
|
|
67
|
-
// This agent's own land-at-completion posture
|
|
67
|
+
// This agent's own land-at-completion posture, an override of the sandbox-wide `autoLand` setting; null
|
|
68
68
|
// clears it back to "inherit". Legal mid-turn on purpose: the setting is read at turn COMPLETION, so
|
|
69
69
|
// flipping it while the agent works is exactly "hold THIS turn's work for review", the press that matters.
|
|
70
70
|
autoLand: oc.route({ method: "POST", path: "/agents/{id}/auto-land" }).input(AgentAutoLandSchema).output(AgentSummarySchema),
|
|
71
|
-
/* THIS conversation's answer to a provider outage
|
|
71
|
+
/* THIS conversation's answer to a provider outage, an override of the sandbox-wide `resumeAfterOutage`
|
|
72
72
|
* setting; null clears it back to "inherit". The chat's offer at the moment a turn dies writes this and
|
|
73
73
|
* never the global: the press happens inside one conversation and means "finish this piece of work", so
|
|
74
|
-
* its honest
|
|
74
|
+
* its honest scope is that conversation. Sandbox > Agent owns the default for everything else.
|
|
75
75
|
*
|
|
76
|
-
* Legal mid-turn, and unlike autoLand it is legal for a WORKSPACE conversation too
|
|
76
|
+
* Legal mid-turn, and unlike autoLand it is legal for a WORKSPACE conversation too, an outage kills a
|
|
77
77
|
* main-tree chat exactly as readily as an isolated one, and there is no branch involved either way. */
|
|
78
78
|
resumeAfterOutage: oc
|
|
79
79
|
.route({ method: "POST", path: "/agents/{id}/resume-after-outage" })
|