@intentic/sandbox-contract 1.223.0 → 1.225.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 +14 -14
- package/dist/agent-catalog.js +3 -3
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.js +43 -43
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/extension-update.js +2 -2
- package/dist/chores/extension-update.js.map +1 -1
- package/dist/chores/fix-deps.js +1 -1
- package/dist/chores/fix-deps.js.map +1 -1
- package/dist/chores/probes.js +1 -1
- package/dist/chores/probes.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/chores/verdict.js +2 -2
- package/dist/chores/verdict.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +41 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.d.ts +80 -0
- package/dist/contracts/exit.contract.d.ts.map +1 -0
- package/dist/contracts/exit.contract.js +13 -0
- package/dist/contracts/exit.contract.js.map +1 -0
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +12 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/events.d.ts +4 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -5
- package/dist/events.js.map +1 -1
- package/dist/fast-tier.d.ts +9 -0
- package/dist/fast-tier.d.ts.map +1 -0
- package/dist/fast-tier.js +19 -0
- package/dist/fast-tier.js.map +1 -0
- package/dist/history-state.js +3 -3
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +214 -68
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts +1 -0
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +5 -0
- package/dist/model-order.js.map +1 -1
- package/dist/output-fields.d.ts.map +1 -1
- package/dist/output-fields.js +2 -2
- package/dist/output-fields.js.map +1 -1
- package/dist/prompt-complexity.d.ts +19 -0
- package/dist/prompt-complexity.d.ts.map +1 -0
- package/dist/prompt-complexity.js +83 -0
- package/dist/prompt-complexity.js.map +1 -0
- package/dist/publish-drafts.js +2 -2
- package/dist/publish-drafts.js.map +1 -1
- package/dist/schemas.d.ts +236 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +116 -2
- package/dist/schemas.js.map +1 -1
- package/dist/workflow-faults.js +3 -3
- package/dist/workflow-faults.js.map +1 -1
- package/dist/workspace-state.d.ts +20 -20
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +20 -20
- package/dist/workspace-state.js.map +1 -1
- package/package.json +5 -5
- package/src/agent-catalog.test.ts +25 -25
- package/src/agent-catalog.ts +81 -81
- package/src/agent-run-model.test.ts +3 -3
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-ledger.test.ts +13 -13
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/chores.ts +109 -109
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +5 -5
- package/src/chores/fix-deps.ts +4 -4
- package/src/chores/probes.test.ts +6 -6
- package/src/chores/probes.ts +17 -17
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.test.ts +3 -3
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.test.ts +20 -20
- package/src/chores/verdict.ts +25 -25
- package/src/contract-lock.test.ts +1 -1
- 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/exit.contract.ts +42 -0
- 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 +8 -8
- package/src/events.test.ts +3 -3
- package/src/events.ts +142 -142
- package/src/fast-tier.test.ts +88 -0
- package/src/fast-tier.ts +72 -0
- package/src/history-state.ts +15 -15
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.test.ts +1 -1
- package/src/hostnames.ts +15 -15
- package/src/index.ts +13 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.test.ts +11 -11
- package/src/model-order.ts +52 -30
- package/src/output-fields.ts +11 -11
- package/src/path-refs.test.ts +4 -4
- package/src/path-refs.ts +6 -6
- package/src/prompt-complexity.test.ts +160 -0
- package/src/prompt-complexity.ts +271 -0
- package/src/publish-drafts.ts +8 -8
- package/src/quick-model.test.ts +11 -11
- package/src/quick-model.ts +15 -15
- package/src/routes.test.ts +11 -5
- package/src/routes.ts +19 -19
- package/src/runtime-state.test.ts +1 -1
- package/src/runtime-state.ts +22 -22
- package/src/schemas.test.ts +8 -8
- package/src/schemas.ts +1238 -921
- package/src/search-globs.test.ts +2 -2
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.test.ts +1 -1
- 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.test.ts +9 -9
- package/src/title.ts +21 -21
- package/src/tunnel-ids.test.ts +3 -3
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.test.ts +3 -3
- package/src/versions.ts +6 -6
- package/src/workflow-faults.test.ts +6 -6
- package/src/workflow-faults.ts +14 -14
- package/src/workspace-state.test.ts +32 -32
- package/src/workspace-state.ts +139 -139
|
@@ -20,7 +20,7 @@ const pkg = (over: Partial<ChorePackage> = {}): ChorePackage => ({
|
|
|
20
20
|
...over,
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
// A repository that is a Node workspace with documents, a pipeline, an image and a Tailwind front-end
|
|
23
|
+
// A repository that is a Node workspace with documents, a pipeline, an image and a Tailwind front-end, so every
|
|
24
24
|
// chore APPLIES by default and each applicability test can turn off exactly the one fact it is about.
|
|
25
25
|
const shape = (over: Partial<ChoreShape> = {}): ChoreShape => ({
|
|
26
26
|
docs: [`docs/architecture/repo.md`],
|
|
@@ -56,7 +56,7 @@ const auditProbe = (names: readonly string[]): ProbeResult =>
|
|
|
56
56
|
const report = (over: Partial<ChoresReport> = {}): ChoresReport => ({
|
|
57
57
|
repos: [{ repo: `app`, probes: [], signals: signals() }],
|
|
58
58
|
ledger: [],
|
|
59
|
-
// Verdicts are about EVIDENCE, never about work in flight
|
|
59
|
+
// Verdicts are about EVIDENCE, never about work in flight: a probe running does not make a chore more or
|
|
60
60
|
// less due, it only makes the panel say so. Empty here because no assertion in this file should depend on it.
|
|
61
61
|
running: [],
|
|
62
62
|
node: `v24.18.0`,
|
|
@@ -111,7 +111,7 @@ describe(`the ledger debounces; it cannot hide`, () => {
|
|
|
111
111
|
...over,
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
test(`a run against this exact evidence leaves the chore due but settled
|
|
114
|
+
test(`a run against this exact evidence leaves the chore due but settled: shown, never badged`, () => {
|
|
115
115
|
const verdict = verdictFor({ ...withAdvisories, ledger: [ledgerEntry()] }, `security-advisories`);
|
|
116
116
|
expect(verdict.state).toBe(`due`);
|
|
117
117
|
expect(verdict.settled).toBe(true);
|
|
@@ -134,7 +134,7 @@ describe(`the ledger debounces; it cannot hide`, () => {
|
|
|
134
134
|
test(`an agent reporting the findings did not hold up clears the chore until the evidence changes`, () => {
|
|
135
135
|
const verdict = verdictFor({ ...withAdvisories, ledger: [ledgerEntry({ outcome: `clean` })] }, `security-advisories`);
|
|
136
136
|
expect(verdict.state).toBe(`clear`);
|
|
137
|
-
expect(verdict.headline).toBe(`Checked
|
|
137
|
+
expect(verdict.headline).toBe(`Checked, the findings did not hold up`);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
test(`a snooze silences a due chore without hiding it, and lapses on its own`, () => {
|
|
@@ -148,8 +148,8 @@ describe(`the ledger debounces; it cannot hide`, () => {
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
/* A chore with a cadence expires its own settlement, so "we looked and chose not to act" cannot silence it
|
|
151
|
-
* for good. Security has no cadence on purpose
|
|
152
|
-
* ninety days passed, it becomes interesting when the advisory set changes
|
|
151
|
+
* for good. Security has no cadence on purpose: an advisory does not become interesting again because
|
|
152
|
+
* ninety days passed, it becomes interesting when the advisory set changes, so its settlement persists. */
|
|
153
153
|
test(`settlement expires with the chore's cadence, and persists for the chores that have none`, () => {
|
|
154
154
|
const dependencies = choreById(`dependencies-outdated`);
|
|
155
155
|
expect(dependencies?.cadenceMs).toBeGreaterThan(0);
|
|
@@ -180,7 +180,7 @@ describe(`a measurement older than the work is not evidence about the work`, ()
|
|
|
180
180
|
test(`a turn that landed after the measurement steps the chore down from due`, () => {
|
|
181
181
|
const verdict = verdictFor({ ...withAdvisories, ledger: [ledgerEntry({ ranAt: NOW - 3_600_000 })] }, `security-advisories`);
|
|
182
182
|
expect(verdict.state).toBe(`stale`);
|
|
183
|
-
// The evidence stays on the row
|
|
183
|
+
// The evidence stays on the row: it is what the reader checks the claim against, and the claim comes off.
|
|
184
184
|
expect(verdict.detail).not.toEqual([]);
|
|
185
185
|
expect(verdict.settled).toBe(false);
|
|
186
186
|
});
|
|
@@ -194,7 +194,7 @@ describe(`a measurement older than the work is not evidence about the work`, ()
|
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
// Re-measuring is the whole cure: the same run, against evidence taken after it, is settled rather than stale.
|
|
197
|
-
test(`re-measuring after the turn restores the verdict
|
|
197
|
+
test(`re-measuring after the turn restores the verdict: due, and now genuinely settled`, () => {
|
|
198
198
|
const remeasured = report({
|
|
199
199
|
repos: [{ repo: `app`, probes: [{ ...auditProbe([`left-pad`]), ranAt: NOW - 60_000 }], signals: signals() }],
|
|
200
200
|
ledger: [ledgerEntry({ ranAt: NOW - 3_600_000 })],
|
|
@@ -223,7 +223,7 @@ describe(`a measurement older than the work is not evidence about the work`, ()
|
|
|
223
223
|
// Every measured row carries when it was taken, so no row can pass off a week-old count as this morning's.
|
|
224
224
|
test(`every measured verdict says when it was measured, and the unmeasurable ones say nothing`, () => {
|
|
225
225
|
expect(verdictFor(withAdvisories, `security-advisories`).measuredAt).toBe(NOW - DAY);
|
|
226
|
-
// A survey rests on no measurement
|
|
226
|
+
// A survey rests on no measurement: it is decided by the calendar, and has nothing to be out of date.
|
|
227
227
|
expect(verdictFor(report(), `standardize-patterns`).measuredAt).toBeUndefined();
|
|
228
228
|
// Nor does a probe that never ran.
|
|
229
229
|
expect(verdictFor(report(), `security-advisories`).measuredAt).toBeUndefined();
|
|
@@ -363,7 +363,7 @@ describe(`the prompts`, () => {
|
|
|
363
363
|
const dueVerdict = () =>
|
|
364
364
|
verdictFor(report({ repos: [{ repo: `app`, probes: [auditProbe([`left-pad`])], signals: signals() }] }), `security-advisories`);
|
|
365
365
|
|
|
366
|
-
/* A prompt that counts without NAMING sends the agent off to re-derive a list we are already holding
|
|
366
|
+
/* A prompt that counts without NAMING sends the agent off to re-derive a list we are already holding: slowly,
|
|
367
367
|
* and against a tree that has moved since. Every measured chore names its artefacts. */
|
|
368
368
|
test(`name the artefacts, not just how many there were`, () => {
|
|
369
369
|
const verdict = dueVerdict();
|
|
@@ -420,7 +420,7 @@ describe(`the prompts`, () => {
|
|
|
420
420
|
expect(verdict.prompt, verdict.chore.id).toBeTypeOf(`string`);
|
|
421
421
|
expect(verdict.digest, verdict.chore.id).not.toBe(``);
|
|
422
422
|
}
|
|
423
|
-
// Each measured chore's own artefact reaches its own prompt
|
|
423
|
+
// Each measured chore's own artefact reaches its own prompt: the regression this whole test exists for.
|
|
424
424
|
const promptFor = (chore: string) => due.find((verdict) => verdict.chore.id === chore)?.prompt ?? ``;
|
|
425
425
|
expect(promptFor(`security-advisories`)).toContain(`left-pad`);
|
|
426
426
|
expect(promptFor(`dependencies-outdated`)).toContain(`vue 1.0.0 → 2.0.0`);
|
|
@@ -432,7 +432,7 @@ describe(`the prompts`, () => {
|
|
|
432
432
|
});
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
-
/* APPLICABILITY
|
|
435
|
+
/* APPLICABILITY, whether the chore is a QUESTION worth asking of this repository, as opposed to whether the
|
|
436
436
|
* answer is yes. Every case here is one where the previous design showed a row that could never be acted on:
|
|
437
437
|
* an offer to re-read documentation that was never written, to slim an image that does not exist, to tighten a
|
|
438
438
|
* pipeline nobody has. Each of those teaches the reader that this list was not written by someone who looked. */
|
|
@@ -468,7 +468,7 @@ describe(`what does not apply here`, () => {
|
|
|
468
468
|
expect(verdictFor(single, `library-overlap`).state).toBe(`not-applicable`);
|
|
469
469
|
});
|
|
470
470
|
|
|
471
|
-
// A survey has no evidence to be absent
|
|
471
|
+
// A survey has no evidence to be absent: "90 days have passed" is true everywhere, so without a gate it
|
|
472
472
|
// fires forever in repositories where its subject does not exist. This is the regression that motivated
|
|
473
473
|
// making `applies` a required field on SurveySpec rather than an optional one.
|
|
474
474
|
test(`a tiny repository is not surveyed for cross-cutting patterns it cannot have`, () => {
|
|
@@ -498,7 +498,7 @@ describe(`what does not apply here`, () => {
|
|
|
498
498
|
|
|
499
499
|
/* THE CAUSES HAVE TO GROUP, and that is a fact about the STRINGS rather than about the gates. The scope strip
|
|
500
500
|
* prints one line per distinct cause with the chores it costs listed beside it, so two gates that both mean
|
|
501
|
-
* "there is no package.json here" and say it in different words print two lines
|
|
501
|
+
* "there is no package.json here" and say it in different words print two lines, and a workspace root, where
|
|
502
502
|
* a dozen chores are ruled out by three facts, is back to the paragraph-per-chore wall this phrasing replaced.
|
|
503
503
|
* Bounded rather than enumerated: a new gate may invent a new cause, it may not invent a new sentence. */
|
|
504
504
|
test(`applicability reasons are bare causes, so the ones that mean the same thing group`, () => {
|
|
@@ -515,7 +515,7 @@ describe(`what does not apply here`, () => {
|
|
|
515
515
|
});
|
|
516
516
|
});
|
|
517
517
|
|
|
518
|
-
/* THE FRONT-END CHORES. Four chores over two probes, tested where they decide something
|
|
518
|
+
/* THE FRONT-END CHORES. Four chores over two probes, tested where they decide something: the share that makes a
|
|
519
519
|
* bundle a finding, the names that make two components one component, and above all the digests, because three of
|
|
520
520
|
* these four measure things that move every time anyone writes a line of markup. */
|
|
521
521
|
const uiProbe = (scan: Partial<UiScan> = {}): ProbeResult =>
|
|
@@ -601,8 +601,8 @@ describe(`idioms the framework has replaced`, () => {
|
|
|
601
601
|
expect(verdict.detail).toEqual([`3 files · the Options API → <script setup> with the Composition API`]);
|
|
602
602
|
});
|
|
603
603
|
|
|
604
|
-
/* A migration in progress is a set that changes on every commit, so digesting the file identities
|
|
605
|
-
* right for the documentation chore, whose set is packages
|
|
604
|
+
/* A migration in progress is a set that changes on every commit, so digesting the file identities, which is
|
|
605
|
+
* right for the documentation chore, whose set is packages: would badge continuously through exactly the
|
|
606
606
|
* period someone is doing the work. The bucketed count moves on real progress and not on daily churn. */
|
|
607
607
|
test(`one more file in a large migration is not news`, () => {
|
|
608
608
|
const before = verdictFor(withProbes([uiProbe({ idioms: [idioms(`vue-options-api`, 40)] })]), `framework-idiom`);
|
|
@@ -668,7 +668,7 @@ describe(`components built twice`, () => {
|
|
|
668
668
|
});
|
|
669
669
|
|
|
670
670
|
/* Half a measurement would let the row claim it looked for shared logic in a repository where jscpd has never
|
|
671
|
-
* run
|
|
671
|
+
* run: the "measured and found nothing" lie the unavailable state exists to prevent. */
|
|
672
672
|
test(`without the clone sweep the chore is unavailable, not clear`, () => {
|
|
673
673
|
expect(verdictFor(withProbes([components(`src/Button.vue`, `src/ui/Button.vue`)]), `component-overlap`).state).toBe(`unavailable`);
|
|
674
674
|
});
|
|
@@ -692,7 +692,7 @@ describe(`hard-coded styles`, () => {
|
|
|
692
692
|
expect(verdict.detail[0]).toBe(`src/Checkout.vue · 11 values`);
|
|
693
693
|
});
|
|
694
694
|
|
|
695
|
-
// Tailwind gates this one alone
|
|
695
|
+
// Tailwind gates this one alone: a Vue repository with no Tailwind has no theme scale to have bypassed, and
|
|
696
696
|
// a row saying so would be the surface inventing a subject.
|
|
697
697
|
test(`a repository without Tailwind is not asked the question at all`, () => {
|
|
698
698
|
const verdict = verdictFor(
|
|
@@ -713,7 +713,7 @@ describe(`hard-coded styles`, () => {
|
|
|
713
713
|
});
|
|
714
714
|
});
|
|
715
715
|
|
|
716
|
-
/* THE CRITERION
|
|
716
|
+
/* THE CRITERION: the rule in words, next to the evidence that met it. A row that reports a number without the
|
|
717
717
|
* rule behind it is asking to be taken on trust, and the first row that turns out to be wrong costs the whole
|
|
718
718
|
* list its credibility. */
|
|
719
719
|
describe(`every chore says what would make it due`, () => {
|
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`;
|
|
@@ -102,16 +102,16 @@ const unmeasuredDetail = (needs: readonly ProbeId[], probes: ReadonlyMap<ProbeId
|
|
|
102
102
|
if (probe.state === `unavailable`) {
|
|
103
103
|
return [`${spec.title} · ${probe.reason ?? `not available in this repository`}`];
|
|
104
104
|
}
|
|
105
|
-
return [`${spec.title} · failed${probe.reason === undefined ? `` :
|
|
105
|
+
return [`${spec.title} · failed${probe.reason === undefined ? `` : `, ${probe.reason}`}`];
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLedgerEntry | undefined): ChoreVerdict => {
|
|
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. */
|
|
@@ -185,7 +185,7 @@ export const assessChore = (chore: Chore, context: ChoreContext, ledger: ChoreLe
|
|
|
185
185
|
...base,
|
|
186
186
|
state: `clear`,
|
|
187
187
|
severity: `info`,
|
|
188
|
-
headline: `Checked
|
|
188
|
+
headline: `Checked, the findings did not hold up`,
|
|
189
189
|
detail: finding.detail,
|
|
190
190
|
digest: finding.digest,
|
|
191
191
|
measuredAt,
|
|
@@ -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
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { expect, test } from "vitest";
|
|
3
3
|
import { currentLock } from "./contract-lock.js";
|
|
4
4
|
|
|
5
|
-
/* The committed lock and the code must say the same thing
|
|
5
|
+
/* The committed lock and the code must say the same thing: see contract-lock.ts for what the pair buys.
|
|
6
6
|
*
|
|
7
7
|
* This is the HALF that runs everywhere the tests run; the other half (a shrunk lock needs a declared break)
|
|
8
8
|
* lives in prepass.mjs, which has git and this suite does not. */
|
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" })
|
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
} from "../schemas.js";
|
|
12
12
|
|
|
13
13
|
// The sandbox's automations manifest (scheduled agent wake-ups). `list` returns each automation with its recent
|
|
14
|
-
// runs + next fire time. `upsert` adds or edits by id (nothing to provision
|
|
14
|
+
// runs + next fire time. `upsert` adds or edits by id (nothing to provision, the scheduler picks it up on its
|
|
15
15
|
// next poll); `setEnabled` changes only the switch, so a list-row action never has to reconstruct the record.
|
|
16
16
|
// `remove` deletes.
|
|
17
17
|
// The `pending*` routes are the owner's approval queue: a `requireApproval` automation holds each fire here
|
|
18
18
|
// instead of waking; `approve` runs the held wake, `reject` drops it.
|
|
19
19
|
export const automationsContract = {
|
|
20
20
|
list: oc.route({ method: "GET", path: "/automations" }).output(AutomationsListSchema),
|
|
21
|
-
/* WHAT CAN WAKE AN AGENT HERE, and what to start from
|
|
21
|
+
/* WHAT CAN WAKE AN AGENT HERE, and what to start from, the daemon's own sources and templates merged with
|
|
22
22
|
* every installed extension's. The composer's entire vocabulary, so that adding a trigger to an area is a
|
|
23
23
|
* change to that area and to nothing else. `upsert` below validates against the same merge, which is what
|
|
24
24
|
* keeps the surface and the daemon from disagreeing about what is allowed. */
|
|
@@ -26,18 +26,18 @@ export const automationsContract = {
|
|
|
26
26
|
upsert: oc.route({ method: "POST", path: "/automations" }).input(AutomationSchema).output(OkSchema),
|
|
27
27
|
setEnabled: oc.route({ method: "POST", path: "/automations/{id}/enabled" }).input(AutomationEnabledInputSchema).output(OkSchema),
|
|
28
28
|
remove: oc.route({ method: "DELETE", path: "/automations/{id}" }).input(AutomationIdParamSchema).output(OkSchema),
|
|
29
|
-
/* Fire one automation NOW, by hand
|
|
29
|
+
/* Fire one automation NOW, by hand, the answer to "I wrote a 3 a.m. cron and I have no way to try it".
|
|
30
30
|
* It runs the SAME path the real trigger runs: a schedule stays a headless main-tree wake, because a
|
|
31
31
|
* test-fire that proves an isolated worktree works proves nothing about the fire it is standing in for. The
|
|
32
32
|
* guard runs too ("skipped by guard" is the most useful thing this can report); only the approval gate is
|
|
33
33
|
* skipped, since pressing the button IS the owner's approval.
|
|
34
34
|
*
|
|
35
|
-
* Owner-explicit, so a DISABLED automation fires as well
|
|
35
|
+
* Owner-explicit, so a DISABLED automation fires as well, trying a prompt before switching it on is the
|
|
36
36
|
* main reason to press this, and unlike the /automations/{id}/fire webhook there is no outside sender here to
|
|
37
37
|
* fail closed against.
|
|
38
38
|
*
|
|
39
39
|
* NOT FOR A LISTENER, which is the one trigger whose fire is nothing without the thing that fired it. A
|
|
40
|
-
* listener's prompt is a brief about handling the events riding with it, and by hand there are none
|
|
40
|
+
* listener's prompt is a brief about handling the events riding with it, and by hand there are none, so the
|
|
41
41
|
* button could only ever produce an agent told to handle events, handed none, asking where they went. Worse,
|
|
42
42
|
* that pointless run took the automation's turn: a real mention arriving while it ran had to wait behind it.
|
|
43
43
|
* Refused here rather than hidden in the UI alone, because the honest answer to "how do I test this" is to
|
|
@@ -19,14 +19,14 @@ import {
|
|
|
19
19
|
/* The sandbox's unified capability manifest. `list` returns each active capability with its live status, the
|
|
20
20
|
* non-secret echo of its config, and the NAMES of the credentials it holds. `add` upserts a capability and
|
|
21
21
|
* STREAMS its apply (devops scaffolding / service provisioning emit ndjson progress; mcp/integration emit a
|
|
22
|
-
* terminal frame), mirroring the /intentic runner. `remove` tears it down (devops refuses
|
|
22
|
+
* terminal frame), mirroring the /intentic runner. `remove` tears it down (devops refuses, deleting the repos
|
|
23
23
|
* is data loss). `status` re-probes a single capability for a lazy UI refresh. `marketplace` resolves a Claude
|
|
24
24
|
* Code plugin marketplace repo into installable plugin-capability configs.
|
|
25
25
|
*
|
|
26
26
|
* `add` IS ALSO THE EDIT, because the write is an upsert: the same id with a changed config changes that
|
|
27
27
|
* connection. A caller editing one has never been shown its credentials, so it sends VAULTED
|
|
28
28
|
* (capability-secrets.ts) for each it is leaving alone and the daemon resolves those from what is stored before
|
|
29
|
-
* anything runs
|
|
29
|
+
* anything runs, the only way to change one setting on a tunnel without re-typing its key. A marker with
|
|
30
30
|
* nothing behind it is refused rather than written. */
|
|
31
31
|
export const capabilitiesContract = {
|
|
32
32
|
list: oc.route({ method: "GET", path: "/capabilities" }).output(CapabilitiesListSchema),
|
|
@@ -34,24 +34,24 @@ export const capabilitiesContract = {
|
|
|
34
34
|
remove: oc.route({ method: "DELETE", path: "/capabilities/{id}" }).input(CapabilityIdParamSchema).output(OkSchema),
|
|
35
35
|
/* Give a connection a different name, carrying what the old one keyed: a browser profile with its logins,
|
|
36
36
|
* a connected machine's enrollment, an extension's checkout. The name is the agent's handle for the thing,
|
|
37
|
-
* so this is a migration
|
|
37
|
+
* so this is a migration, add + remove would lose exactly the state that makes the connection worth
|
|
38
38
|
* keeping. A kind whose name is part of what it IS (the scaffolders, the one-per-sandbox cards) refuses. */
|
|
39
39
|
rename: oc.route({ method: "POST", path: "/capabilities/{id}/rename" }).input(CapabilityRenameSchema).output(OkSchema),
|
|
40
40
|
// Replace just the secret in a capability's config (the /secrets page's edit) and re-run its apply.
|
|
41
41
|
setSecret: oc.route({ method: "POST", path: "/capabilities/{id}/secret" }).input(CapabilitySecretInputSchema).output(OkSchema),
|
|
42
42
|
status: oc.route({ method: "GET", path: "/capabilities/{id}/status" }).input(CapabilityIdParamSchema).output(CapabilityStatusSchema),
|
|
43
|
-
/* One capability's stored config, secrets included
|
|
43
|
+
/* One capability's stored config, secrets included, how an extension BACKEND dials the service behind a
|
|
44
44
|
* connected capability (ext-deployments reads its Komodo's key pair through this). Never a browser's: the
|
|
45
45
|
* handler refuses any caller with a member identity, so only the daemon's header grants reach it, and an
|
|
46
|
-
* extension's grant reaches it only when its manifest declares the route in `permissions.daemon
|
|
46
|
+
* extension's grant reaches it only when its manifest declares the route in `permissions.daemon`, which
|
|
47
47
|
* is the install dialog saying, in one line, "this extension can read connected credentials". */
|
|
48
48
|
connection: oc.route({ method: "GET", path: "/capabilities/{id}/connection" }).input(CapabilityIdParamSchema).output(CapabilityConnectionSchema),
|
|
49
49
|
marketplace: oc.route({ method: "POST", path: "/capabilities/marketplace" }).input(MarketplaceRequestSchema).output(MarketplaceSchema),
|
|
50
50
|
// "Not needed": stop offering this card until the workspace evidence behind it changes. Nothing is torn
|
|
51
|
-
// down and nothing is remembered about the card itself
|
|
51
|
+
// down and nothing is remembered about the card itself, only the evidence it was declined against.
|
|
52
52
|
dismiss: oc.route({ method: "DELETE", path: "/capabilities/recommendations/{card}" }).input(CapabilityCardParamSchema).output(OkSchema),
|
|
53
53
|
// Start an agent-kind capability's interactive login (its declared loginCommand) in a visible terminal
|
|
54
|
-
// session the user types into
|
|
54
|
+
// session the user types into, device-code sign-in flows. Returns the session the panel attaches to.
|
|
55
55
|
login: oc.route({ method: "POST", path: "/capabilities/{id}/login" }).input(CapabilityIdParamSchema).output(CapabilityLoginSchema),
|
|
56
56
|
// Mint one TOTP code from the capability's stored seed (a field its card marks `totp`). The one capability
|
|
57
57
|
// read the agent token is admitted to (see auth/grants): a code expires within its period and never reveals
|
|
@@ -2,19 +2,19 @@ import { oc } from "@orpc/contract";
|
|
|
2
2
|
import { ChoreLedgerWriteSchema, ChoreProbeRequestSchema, ChoresReportSchema, OkSchema } from "../schemas.js";
|
|
3
3
|
|
|
4
4
|
/* Maintenance evidence: what every repo under /work currently measures, and what has already been done about it.
|
|
5
|
-
* Three routes, because there are exactly three things the surface does
|
|
5
|
+
* Three routes, because there are exactly three things the surface does, read the evidence, ask for a
|
|
6
6
|
* measurement to be retaken, and record what a turn concluded.
|
|
7
7
|
*
|
|
8
8
|
* There is no `GET /chores/{id}` and no "run this chore" route on purpose. A chore RUN is an ordinary isolated
|
|
9
9
|
* fleet agent (`POST /agent` with a derived conversation id), the same as an acceptance run or a documentation
|
|
10
|
-
* generation
|
|
10
|
+
* generation, so the worktree, the live status, the cost, the transcript and the /agents/<id> page already
|
|
11
11
|
* exist, and adding a bespoke launcher here would be a second way to start a turn that has to be kept in step
|
|
12
12
|
* with the first. */
|
|
13
13
|
export const choresContract = {
|
|
14
14
|
// Every repo's standing evidence in one read: cached probe results (with their age and state), the cheap
|
|
15
15
|
// resident signals, the ledger, and the daemon's node version. The rail badge polls this; so does the panel.
|
|
16
16
|
list: oc.route({ method: "GET", path: "/chores" }).output(ChoresReportSchema),
|
|
17
|
-
// Re-run one repo's probe now, ignoring its TTL
|
|
17
|
+
// Re-run one repo's probe now, ignoring its TTL, the panel's per-probe refresh. An ack: the runner works in
|
|
18
18
|
// the background and the result arrives on the next `list`, because a jscpd sweep outlives any sane request.
|
|
19
19
|
probe: oc.route({ method: "POST", path: "/chores/probe" }).input(ChoreProbeRequestSchema).output(OkSchema),
|
|
20
20
|
// Record what a chore turn concluded, or snooze one. Upsert by repo+chore: a chore has one current verdict,
|