@intentic/sandbox-contract 1.176.2 → 1.209.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 +65 -0
- package/dist/agent-catalog.d.ts +15 -2
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +50 -1
- package/dist/agent-catalog.js.map +1 -1
- package/dist/agent-run-model.d.ts +3 -0
- package/dist/agent-run-model.d.ts.map +1 -0
- package/dist/agent-run-model.js +10 -0
- package/dist/agent-run-model.js.map +1 -0
- package/dist/capability-env.d.ts +2 -0
- package/dist/capability-env.d.ts.map +1 -0
- package/dist/capability-env.js +2 -0
- package/dist/capability-env.js.map +1 -0
- package/dist/chores/chores.d.ts +9 -0
- package/dist/chores/chores.d.ts.map +1 -1
- package/dist/chores/chores.js +257 -18
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/digest.js +0 -0
- package/dist/chores/digest.js.map +1 -1
- package/dist/chores/extension-update.d.ts +9 -0
- package/dist/chores/extension-update.d.ts.map +1 -0
- package/dist/chores/extension-update.js +15 -0
- package/dist/chores/extension-update.js.map +1 -0
- package/dist/chores/fix-deps.d.ts +10 -0
- package/dist/chores/fix-deps.d.ts.map +1 -0
- package/dist/chores/fix-deps.js +18 -0
- package/dist/chores/fix-deps.js.map +1 -0
- package/dist/chores/index.d.ts +7 -2
- package/dist/chores/index.d.ts.map +1 -1
- package/dist/chores/index.js +4 -1
- package/dist/chores/index.js.map +1 -1
- package/dist/chores/probes.d.ts +1 -0
- package/dist/chores/probes.d.ts.map +1 -1
- package/dist/chores/probes.js +131 -3
- package/dist/chores/probes.js.map +1 -1
- package/dist/chores/stack.d.ts +27 -0
- package/dist/chores/stack.d.ts.map +1 -0
- package/dist/chores/stack.js +121 -0
- package/dist/chores/stack.js.map +1 -0
- package/dist/chores/verdict.d.ts +2 -1
- package/dist/chores/verdict.d.ts.map +1 -1
- package/dist/chores/verdict.js +57 -7
- package/dist/chores/verdict.js.map +1 -1
- package/dist/contract-lock.d.ts +3 -0
- package/dist/contract-lock.d.ts.map +1 -0
- package/dist/contract-lock.js +35 -0
- package/dist/contract-lock.js.map +1 -0
- package/dist/contracts/activity.contract.d.ts +10 -0
- package/dist/contracts/activity.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +230 -20
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +2 -1
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +847 -1
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +3 -1
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/automations.contract.d.ts +89 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.js +3 -1
- package/dist/contracts/automations.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +108 -32
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/capabilities.contract.js +5 -1
- package/dist/contracts/capabilities.contract.js.map +1 -1
- package/dist/contracts/chores.contract.d.ts +43 -0
- package/dist/contracts/chores.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.d.ts +5 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +2 -2
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/claude.contract.d.ts +3 -14
- package/dist/contracts/claude.contract.d.ts.map +1 -1
- package/dist/contracts/claude.contract.js +2 -3
- package/dist/contracts/claude.contract.js.map +1 -1
- package/dist/contracts/drafts.contract.d.ts +4 -0
- package/dist/contracts/drafts.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.d.ts +28 -12
- package/dist/contracts/endpoints.contract.d.ts.map +1 -1
- package/dist/contracts/endpoints.contract.js +9 -0
- package/dist/contracts/endpoints.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +399 -69
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.js +15 -1
- package/dist/contracts/extensions.contract.js.map +1 -1
- package/dist/contracts/git.contract.d.ts +209 -11
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +12 -2
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/grok.contract.d.ts +0 -13
- package/dist/contracts/grok.contract.d.ts.map +1 -1
- package/dist/contracts/grok.contract.js +1 -2
- package/dist/contracts/grok.contract.js.map +1 -1
- package/dist/contracts/host.contract.d.ts +39 -0
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.js +3 -2
- package/dist/contracts/host.contract.js.map +1 -1
- package/dist/contracts/loops.contract.d.ts +343 -0
- package/dist/contracts/loops.contract.d.ts.map +1 -0
- package/dist/contracts/loops.contract.js +11 -0
- package/dist/contracts/loops.contract.js.map +1 -0
- package/dist/contracts/panels.contract.d.ts +6 -0
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/personas.contract.d.ts +112 -0
- package/dist/contracts/personas.contract.d.ts.map +1 -0
- package/dist/contracts/personas.contract.js +13 -0
- package/dist/contracts/personas.contract.js.map +1 -0
- package/dist/contracts/ports.contract.d.ts +1 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/contracts/prepush.contract.d.ts +1 -0
- package/dist/contracts/prepush.contract.d.ts.map +1 -1
- package/dist/contracts/prepush.contract.js.map +1 -1
- package/dist/contracts/{codex.contract.d.ts → providers.contract.d.ts} +11 -3
- package/dist/contracts/providers.contract.d.ts.map +1 -0
- package/dist/contracts/providers.contract.js +6 -0
- package/dist/contracts/providers.contract.js.map +1 -0
- package/dist/contracts/public.contract.d.ts +24 -0
- package/dist/contracts/public.contract.d.ts.map +1 -0
- package/dist/contracts/public.contract.js +8 -0
- package/dist/contracts/public.contract.js.map +1 -0
- package/dist/contracts/push.contract.d.ts +3 -3
- package/dist/contracts/secrets.contract.d.ts +9 -0
- package/dist/contracts/secrets.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +16 -1
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.js +1 -1
- package/dist/contracts/sessions.contract.js.map +1 -1
- package/dist/contracts/settings.contract.d.ts +352 -58
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +2 -1
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/share.contract.d.ts +55 -0
- package/dist/contracts/share.contract.d.ts.map +1 -0
- package/dist/contracts/share.contract.js +9 -0
- package/dist/contracts/share.contract.js.map +1 -0
- package/dist/contracts/skills.contract.d.ts +41 -0
- package/dist/contracts/skills.contract.d.ts.map +1 -0
- package/dist/contracts/skills.contract.js +9 -0
- package/dist/contracts/skills.contract.js.map +1 -0
- package/dist/contracts/system.contract.d.ts +218 -22
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +10 -2
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/workflows.contract.d.ts +575 -0
- package/dist/contracts/workflows.contract.d.ts.map +1 -0
- package/dist/contracts/workflows.contract.js +13 -0
- package/dist/contracts/workflows.contract.js.map +1 -0
- package/dist/contracts/workspace.contract.d.ts +18 -4
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +6 -3
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/conversation-ids.d.ts +2 -0
- package/dist/conversation-ids.d.ts.map +1 -0
- package/dist/conversation-ids.js +113 -0
- package/dist/conversation-ids.js.map +1 -0
- package/dist/events.d.ts +523 -17
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +106 -13
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts +3 -0
- package/dist/history-state.d.ts.map +1 -0
- package/dist/history-state.js +53 -0
- package/dist/history-state.js.map +1 -0
- package/dist/hostnames.d.ts +4 -0
- package/dist/hostnames.d.ts.map +1 -1
- package/dist/hostnames.js +7 -2
- package/dist/hostnames.js.map +1 -1
- package/dist/index.d.ts +3793 -478
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -17
- package/dist/index.js.map +1 -1
- package/dist/listener-protocol.d.ts +77 -0
- package/dist/listener-protocol.d.ts.map +1 -0
- package/dist/listener-protocol.js +33 -0
- package/dist/listener-protocol.js.map +1 -0
- package/dist/model-order.d.ts +1 -0
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +16 -0
- package/dist/model-order.js.map +1 -1
- package/dist/output-fields.d.ts +30 -0
- package/dist/output-fields.d.ts.map +1 -0
- package/dist/output-fields.js +57 -0
- package/dist/output-fields.js.map +1 -0
- package/dist/path-refs.d.ts.map +1 -1
- package/dist/path-refs.js.map +1 -1
- package/dist/publish-drafts.d.ts +7 -0
- package/dist/publish-drafts.d.ts.map +1 -0
- package/dist/publish-drafts.js +20 -0
- package/dist/publish-drafts.js.map +1 -0
- package/dist/quick-model.d.ts +2 -1
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +18 -14
- package/dist/quick-model.js.map +1 -1
- package/dist/routes.d.ts +3 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +63 -0
- package/dist/routes.js.map +1 -1
- package/dist/runtime-state.d.ts +33 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +14 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/schemas.d.ts +4938 -866
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +1011 -192
- package/dist/schemas.js.map +1 -1
- package/dist/search-globs.d.ts +6 -0
- package/dist/search-globs.d.ts.map +1 -0
- package/dist/search-globs.js +39 -0
- package/dist/search-globs.js.map +1 -0
- package/dist/share-paths.d.ts +8 -0
- package/dist/share-paths.d.ts.map +1 -0
- package/dist/share-paths.js +21 -0
- package/dist/share-paths.js.map +1 -0
- package/dist/state-portability.d.ts +8 -0
- package/dist/state-portability.d.ts.map +1 -0
- package/dist/state-portability.js +4 -0
- package/dist/state-portability.js.map +1 -0
- package/dist/title.d.ts.map +1 -1
- package/dist/title.js +1 -1
- package/dist/title.js.map +1 -1
- package/dist/tunnel-ids.d.ts +1 -0
- package/dist/tunnel-ids.d.ts.map +1 -1
- package/dist/tunnel-ids.js +1 -0
- package/dist/tunnel-ids.js.map +1 -1
- package/dist/versions.d.ts +4 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +15 -0
- package/dist/versions.js.map +1 -0
- package/dist/workflow-faults.d.ts +4 -0
- package/dist/workflow-faults.d.ts.map +1 -0
- package/dist/workflow-faults.js +115 -0
- package/dist/workflow-faults.js.map +1 -0
- package/dist/workspace-state.d.ts +282 -3
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +198 -12
- package/dist/workspace-state.js.map +1 -1
- package/package.json +30 -6
- package/src/agent-catalog.test.ts +147 -7
- package/src/agent-catalog.ts +233 -21
- package/src/agent-run-model.test.ts +60 -0
- package/src/agent-run-model.ts +51 -0
- package/src/capability-env.ts +10 -0
- package/src/capability-ledger.test.ts +110 -0
- package/src/chores/chores.test.ts +32 -0
- package/src/chores/chores.ts +456 -32
- package/src/chores/digest.ts +0 -0
- package/src/chores/extension-update.ts +37 -0
- package/src/chores/fix-deps.ts +40 -0
- package/src/chores/index.ts +7 -2
- package/src/chores/probes.test.ts +155 -3
- package/src/chores/probes.ts +209 -4
- package/src/chores/stack.test.ts +126 -0
- package/src/chores/stack.ts +244 -0
- package/src/chores/verdict.test.ts +357 -19
- package/src/chores/verdict.ts +102 -22
- package/src/contract-lock.test.ts +17 -0
- package/src/contract-lock.ts +66 -0
- package/src/contracts/agent.contract.ts +6 -0
- package/src/contracts/agents.contract.ts +12 -0
- package/src/contracts/automations.contract.ts +17 -1
- package/src/contracts/capabilities.contract.ts +22 -0
- package/src/contracts/ci.contract.ts +10 -2
- package/src/contracts/claude.contract.ts +8 -4
- package/src/contracts/endpoints.contract.ts +21 -0
- package/src/contracts/extensions.contract.ts +42 -0
- package/src/contracts/git.contract.ts +33 -6
- package/src/contracts/grok.contract.ts +4 -3
- package/src/contracts/host.contract.ts +13 -2
- package/src/contracts/loops.contract.ts +62 -0
- package/src/contracts/personas.contract.ts +56 -0
- package/src/contracts/prepush.contract.ts +4 -3
- package/src/contracts/providers.contract.ts +23 -0
- package/src/contracts/public.contract.ts +14 -0
- package/src/contracts/sessions.contract.ts +5 -1
- package/src/contracts/settings.contract.ts +5 -0
- package/src/contracts/share.contract.ts +20 -0
- package/src/contracts/skills.contract.ts +24 -0
- package/src/contracts/system.contract.ts +27 -3
- package/src/contracts/workflows.contract.ts +78 -0
- package/src/contracts/workspace.contract.ts +10 -2
- package/src/conversation-ids.test.ts +25 -0
- package/src/conversation-ids.ts +160 -0
- package/src/events.test.ts +20 -1
- package/src/events.ts +404 -43
- package/src/history-state.ts +129 -0
- package/src/hostnames.ts +20 -6
- package/src/index.ts +60 -17
- package/src/listener-protocol.ts +96 -0
- package/src/model-order.test.ts +32 -1
- package/src/model-order.ts +56 -3
- package/src/output-fields.ts +111 -0
- package/src/path-refs.test.ts +4 -4
- package/src/path-refs.ts +5 -5
- package/src/publish-drafts.ts +59 -0
- package/src/quick-model.test.ts +70 -27
- package/src/quick-model.ts +76 -40
- package/src/routes.test.ts +91 -3
- package/src/routes.ts +135 -0
- package/src/runtime-state.test.ts +45 -0
- package/src/runtime-state.ts +124 -0
- package/src/schemas.test.ts +16 -29
- package/src/schemas.ts +3526 -553
- package/src/search-globs.test.ts +46 -0
- package/src/search-globs.ts +76 -0
- package/src/share-paths.test.ts +48 -0
- package/src/share-paths.ts +68 -0
- package/src/state-portability.ts +56 -0
- package/src/title.test.ts +20 -1
- package/src/title.ts +12 -3
- package/src/tunnel-ids.ts +15 -1
- package/src/versions.test.ts +39 -0
- package/src/versions.ts +48 -0
- package/src/workflow-faults.test.ts +127 -0
- package/src/workflow-faults.ts +188 -0
- package/src/workspace-state.test.ts +189 -17
- package/src/workspace-state.ts +500 -30
- package/dist/contracts/codex.contract.d.ts.map +0 -1
- package/dist/contracts/codex.contract.js +0 -6
- package/dist/contracts/codex.contract.js.map +0 -1
- package/dist/contracts/gemini.contract.d.ts +0 -16
- package/dist/contracts/gemini.contract.d.ts.map +0 -1
- package/dist/contracts/gemini.contract.js +0 -6
- package/dist/contracts/gemini.contract.js.map +0 -1
- package/dist/contracts/kimi.contract.d.ts +0 -16
- package/dist/contracts/kimi.contract.d.ts.map +0 -1
- package/dist/contracts/kimi.contract.js +0 -6
- package/dist/contracts/kimi.contract.js.map +0 -1
- package/dist/contracts/komodo.contract.d.ts +0 -125
- package/dist/contracts/komodo.contract.d.ts.map +0 -1
- package/dist/contracts/komodo.contract.js +0 -14
- package/dist/contracts/komodo.contract.js.map +0 -1
- package/dist/contracts/memory.contract.d.ts +0 -34
- package/dist/contracts/memory.contract.d.ts.map +0 -1
- package/dist/contracts/memory.contract.js +0 -9
- package/dist/contracts/memory.contract.js.map +0 -1
- package/src/contracts/codex.contract.ts +0 -10
- package/src/contracts/gemini.contract.ts +0 -11
- package/src/contracts/kimi.contract.ts +0 -9
- package/src/contracts/komodo.contract.ts +0 -36
- package/src/contracts/memory.contract.ts +0 -13
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* WHY A WORKFLOW GRAPH IS NOT RUNNABLE — the rules that `WorkflowSchema` cannot state, because each of them is
|
|
2
|
+
* about the graph rather than about one field.
|
|
3
|
+
*
|
|
4
|
+
* A rule per function, so the reason a graph is refused reads as one sentence in one place. They are
|
|
5
|
+
* concatenated in the order the user meets them: what is wrong with the ids, then what is wrong with each step,
|
|
6
|
+
* then what is wrong BETWEEN steps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { duplicateOutputFieldNames } from "./output-fields.js";
|
|
10
|
+
import type { Workflow, WorkflowStep } from "./schemas.js";
|
|
11
|
+
|
|
12
|
+
// One id, one node: the scheduler keys needs, runs and the drawn graph by it, so a repeat makes the same node
|
|
13
|
+
// mean two things.
|
|
14
|
+
const duplicateIdFaults = (steps: readonly WorkflowStep[]): string[] => {
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
const faults: string[] = [];
|
|
17
|
+
for (const step of steps) {
|
|
18
|
+
if (seen.has(step.id)) {
|
|
19
|
+
faults.push(`Two steps share the id "${step.id}".`);
|
|
20
|
+
}
|
|
21
|
+
seen.add(step.id);
|
|
22
|
+
}
|
|
23
|
+
return faults;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// A step that continues a session needs exactly one session to continue. Zero (a root) has nothing to carry on
|
|
27
|
+
// from; two would have to pick, and picking silently is how a workflow quietly drops half its context.
|
|
28
|
+
const continuesOneSession = (step: WorkflowStep): boolean => step.handoff === "continue" && step.needs.length === 1;
|
|
29
|
+
|
|
30
|
+
// What is wrong with this step on its own, given the ids the workflow actually has.
|
|
31
|
+
const stepFaults = (step: WorkflowStep, ids: ReadonlySet<string>): string[] => {
|
|
32
|
+
const faults = step.needs
|
|
33
|
+
.filter((need) => !ids.has(need))
|
|
34
|
+
.map((need) => `"${step.title}" waits for "${need}", which is not a step in this workflow.`);
|
|
35
|
+
if (step.needs.includes(step.id)) {
|
|
36
|
+
faults.push(`"${step.title}" waits for itself.`);
|
|
37
|
+
}
|
|
38
|
+
if (step.handoff === "continue" && !continuesOneSession(step)) {
|
|
39
|
+
faults.push(
|
|
40
|
+
step.needs.length === 0
|
|
41
|
+
? `"${step.title}" continues a session but starts the run — there is nothing to continue.`
|
|
42
|
+
: `"${step.title}" continues a session but waits for ${step.needs.length} steps; it can only continue one.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (step.output.kind === "json") {
|
|
46
|
+
faults.push(
|
|
47
|
+
...duplicateOutputFieldNames(step.output.fields).map(
|
|
48
|
+
(name) => `"${step.title}" declares the output field "${name}" more than once; field names must be unique.`,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
/* A STEP THAT DECLARES NOTHING IS NOT A FAULT — it is the ordinary step, and this used to refuse it.
|
|
53
|
+
*
|
|
54
|
+
* The rule was borrowed from loops (`loopCanConverge`), where it is right: a LOOP is started to repeat
|
|
55
|
+
* until something is true, so one with nothing to produce and nothing to check has no reason to run twice
|
|
56
|
+
* and the dialog is correct to grey out its button. A STEP is not started to repeat. It is one agent
|
|
57
|
+
* session with a job, and the job being done is the turn ending — which is exactly what the loop machinery
|
|
58
|
+
* already does with it (loop-stop's `readDocument` answers `done` for a `none` output, so iteration 1 is
|
|
59
|
+
* the only iteration).
|
|
60
|
+
*
|
|
61
|
+
* Keeping the rule here forced every step to declare an output or a check before the graph would save, and
|
|
62
|
+
* the cheapest way to satisfy it was a `claim` — which buys a verdict file nobody reads, a page of contract
|
|
63
|
+
* in the prompt, and a way for a step that did the work to fail for not having described it.
|
|
64
|
+
*/
|
|
65
|
+
return faults;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/* Two steps continuing the SAME session is the one graph that is legal on paper and broken in practice: both
|
|
69
|
+
* would run on one conversation, in parallel, against one worktree and one turn mutex — so they would serialize
|
|
70
|
+
* on a lock neither knows about and the second would inherit a session the first had moved on. A predecessor
|
|
71
|
+
* can be continued once; anything else that needs its result takes it as a handover. */
|
|
72
|
+
const sharedContinuationFaults = (steps: readonly WorkflowStep[]): string[] => {
|
|
73
|
+
const continued = new Map<string, string[]>();
|
|
74
|
+
for (const step of steps.filter(continuesOneSession)) {
|
|
75
|
+
const parent = step.needs[0] ?? "";
|
|
76
|
+
const titles = continued.get(parent) ?? [];
|
|
77
|
+
titles.push(step.title);
|
|
78
|
+
continued.set(parent, titles);
|
|
79
|
+
}
|
|
80
|
+
return [...continued]
|
|
81
|
+
.filter(([, titles]) => titles.length > 1)
|
|
82
|
+
.map(([parent, titles]) => `${titles.map((title) => `"${title}"`).join(" and ")} all continue "${parent}"'s session; only one step can.`);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Cycles, by walking every path from every root. A workflow with a cycle has steps that can never start, and the
|
|
86
|
+
// scheduler would simply wait forever on them rather than saying so.
|
|
87
|
+
const cycleFaults = (steps: readonly WorkflowStep[]): string[] => {
|
|
88
|
+
const needsById = new Map(steps.map((step) => [step.id, step.needs]));
|
|
89
|
+
const state = new Map<string, "open" | "closed">();
|
|
90
|
+
const faults: string[] = [];
|
|
91
|
+
const walk = (id: string, trail: readonly string[]): void => {
|
|
92
|
+
if (state.get(id) === "closed") {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (state.get(id) === "open") {
|
|
96
|
+
faults.push(`These steps wait for each other in a circle: ${[...trail.slice(trail.indexOf(id)), id].join(" → ")}.`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
state.set(id, "open");
|
|
100
|
+
for (const need of needsById.get(id) ?? []) {
|
|
101
|
+
if (needsById.has(need)) {
|
|
102
|
+
walk(need, [...trail, id]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
state.set(id, "closed");
|
|
106
|
+
};
|
|
107
|
+
for (const step of steps) {
|
|
108
|
+
walk(step.id, []);
|
|
109
|
+
}
|
|
110
|
+
return faults;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/* A GATE THAT CANNOT BE ANSWERED. Every rule here is about the gate against the GRAPH — which is why none of
|
|
114
|
+
* them can live in the schema — and all of them fail the same expensive way if unchecked: the run spends its
|
|
115
|
+
* whole fan-out of sessions and then answers `blocked`, on every commit, for a reason nobody sees until they
|
|
116
|
+
* go reading the daemon's log.
|
|
117
|
+
*
|
|
118
|
+
* The `string[]` refusal is the least obvious and the most worth having. A release decision is one value
|
|
119
|
+
* compared against a list of values that ship; a field holding several has no such reading, and the one the
|
|
120
|
+
* gate would fall into (join them and compare the string) is a rule nobody wrote down and nobody could guess.
|
|
121
|
+
*/
|
|
122
|
+
const gateFaults = (workflow: Pick<Workflow, "steps" | "gate">): string[] => {
|
|
123
|
+
const { gate } = workflow;
|
|
124
|
+
if (gate === undefined) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const step = workflow.steps.find((entry) => entry.id === gate.step);
|
|
128
|
+
if (step === undefined) {
|
|
129
|
+
return [`The gate reads step "${gate.step}", which is not a step in this workflow.`];
|
|
130
|
+
}
|
|
131
|
+
if (step.output.kind !== "json") {
|
|
132
|
+
return [`The gate reads "${step.title}", but that step declares no output fields for it to read.`];
|
|
133
|
+
}
|
|
134
|
+
const field = step.output.fields.find((entry) => entry.name === gate.field);
|
|
135
|
+
if (field === undefined) {
|
|
136
|
+
return [`The gate reads "${gate.field}", which "${step.title}" does not declare.`];
|
|
137
|
+
}
|
|
138
|
+
if (field.type === "string[]") {
|
|
139
|
+
return [`The gate reads "${gate.field}", which is a list — a release decision has to be one value.`];
|
|
140
|
+
}
|
|
141
|
+
// The schema refuses this on save (pass is min(1)), but the designer edits drafts the schema never sees —
|
|
142
|
+
// and an empty allowlist is a gate no run could ever answer "pass", which deserves a sentence, not a save error.
|
|
143
|
+
if (gate.pass.length === 0) {
|
|
144
|
+
return [`The gate names no passing values, so no run could ever ship.`];
|
|
145
|
+
}
|
|
146
|
+
// A field the step may legally omit is a gate that answers `blocked` whenever it does, which is a release
|
|
147
|
+
// stuck on a technicality rather than on the product.
|
|
148
|
+
return field.required ? [] : [`The gate reads "${gate.field}", which "${step.title}" declares optional — it has to be required.`];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/* Why the graph is not runnable, as a list of sentences. Empty ⇒ it is. Shared by the save route (which
|
|
152
|
+
* refuses) and the designer (which shows them under the canvas as you type), because a rule enforced only
|
|
153
|
+
* daemon-side is a rule the user meets as a failed save with no idea which node is wrong.
|
|
154
|
+
*/
|
|
155
|
+
export const workflowFaults = (workflow: Pick<Workflow, "steps" | "gate">): string[] => {
|
|
156
|
+
const ids = new Set(workflow.steps.map((step) => step.id));
|
|
157
|
+
return [
|
|
158
|
+
...duplicateIdFaults(workflow.steps),
|
|
159
|
+
...workflow.steps.flatMap((step) => stepFaults(step, ids)),
|
|
160
|
+
...sharedContinuationFaults(workflow.steps),
|
|
161
|
+
...cycleFaults(workflow.steps),
|
|
162
|
+
...gateFaults(workflow),
|
|
163
|
+
];
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/* WHAT ONLY A RUN CAN BE WRONG ABOUT — kept apart from the rules above because it is not about the graph, and
|
|
167
|
+
* the graph is what gets SAVED. A design whose steps take their goal and instruction from the request is a
|
|
168
|
+
* perfectly good design; it is only unrunnable on the particular run that forgot to bring one.
|
|
169
|
+
*
|
|
170
|
+
* Which is why this cannot be a save-time rule and must not become one: refusing to save such a workflow would
|
|
171
|
+
* outlaw the entire point of a workflow being a SHAPE. The check belongs at the two doors that start runs — the
|
|
172
|
+
* run route and the gate's webhook — and it has to be there rather than left to fail later, because "later"
|
|
173
|
+
* means every session in the fan-out has already been paid for before anyone finds out the model was handed an
|
|
174
|
+
* empty instruction.
|
|
175
|
+
*/
|
|
176
|
+
export const workflowRunFaults = (workflow: Pick<Workflow, "steps">, request: string | undefined): string[] => {
|
|
177
|
+
if (request !== undefined && request.trim() !== "") {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const inheriting = workflow.steps.filter((step) => step.goal === undefined || step.prompt === undefined);
|
|
181
|
+
if (inheriting.length === 0) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
return [
|
|
185
|
+
`${inheriting.map((step) => `"${step.title}"`).join(", ")} take their goal or their instruction from what the run was asked to do, ` +
|
|
186
|
+
`and this run was started without a request. Say what you want built, or give those steps a goal and a prompt of their own.`,
|
|
187
|
+
];
|
|
188
|
+
};
|
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { STATE_DIR } from "@intentic/constants";
|
|
2
|
+
import type { FileContribution } from "@intentic/extension-manifest";
|
|
2
3
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isLockedWorkspacePath,
|
|
6
|
+
isReportedManifest,
|
|
7
|
+
REPORTED_MANIFEST_PATHS,
|
|
8
|
+
staleQueryKeys,
|
|
9
|
+
VERSIONED_STATE_PATHS,
|
|
10
|
+
WORKSPACE_STATE_FILES,
|
|
11
|
+
} from "./workspace-state.js";
|
|
4
12
|
|
|
5
13
|
// What the automations extension declares in its manifest, and what the memory extension WOULD declare if the
|
|
6
14
|
// watcher reported its files. Literals rather than the real manifests: an extension package importing this one is
|
|
7
15
|
// the dependency direction, so reaching back for them here would invert it. The real manifests are checked
|
|
8
16
|
// against this rule where they are loaded — web's fileBindings.test.ts and the daemon's file-bindings.test.ts.
|
|
9
17
|
const AUTOMATIONS: readonly FileContribution[] = [
|
|
10
|
-
{ path:
|
|
11
|
-
{ path:
|
|
18
|
+
{ path: `${STATE_DIR}/automations.json`, invalidates: [`automations`] },
|
|
19
|
+
{ path: `${STATE_DIR}/approvals/`, invalidates: [`automation-approvals`] },
|
|
12
20
|
];
|
|
13
21
|
|
|
14
22
|
describe(`staleQueryKeys`, () => {
|
|
15
23
|
it(`maps a manifest write to the queries it makes stale`, () => {
|
|
16
|
-
expect(staleQueryKeys([`.intentic/capabilities.json`], [])).toEqual([`capabilities`, `environment`, `panels`]);
|
|
24
|
+
expect(staleQueryKeys([`.intentic/capabilities.json`], [])).toEqual([`capabilities`, `environment`, `panels`, `manifests`]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it(`refreshes the unreadable-manifest notice for the three files a person hand-edits`, () => {
|
|
28
|
+
// Only these three. Every manifest reports what the daemon could not read in it, but a write to one of
|
|
29
|
+
// the twenty-odd DAEMON-written ones cannot introduce a typo, and billing every browser a refetch for
|
|
30
|
+
// each of them is the amplification this table exists to avoid.
|
|
31
|
+
const carries = WORKSPACE_STATE_FILES.filter((file) => file.invalidates.includes(`manifests`)).map((file) => file.path);
|
|
32
|
+
expect(carries.toSorted()).toEqual([`.intentic/capabilities.json`, `.intentic/personas.json`, `.intentic/settings.json`]);
|
|
17
33
|
});
|
|
18
34
|
|
|
19
35
|
it(`matches a name family and a one-file-per-entry directory through one prefix each`, () => {
|
|
@@ -32,14 +48,14 @@ describe(`staleQueryKeys`, () => {
|
|
|
32
48
|
it(`ignores unrelated churn under .intentic/`, () => {
|
|
33
49
|
// The amplification that turned an iq index rebuild into an endless request storm: a prefix test on
|
|
34
50
|
// `.intentic/` alone would invalidate every one of these queries for each index write.
|
|
35
|
-
expect(staleQueryKeys([`.intentic/iq/index.db`, `.intentic/claude/projects/p/session.jsonl`], [])).toEqual([]);
|
|
51
|
+
expect(staleQueryKeys([`.intentic/cache/iq/index.db`, `.intentic/sessions/claude/projects/p/session.jsonl`], [])).toEqual([]);
|
|
36
52
|
});
|
|
37
53
|
|
|
38
54
|
it(`ignores a store's own temp file while it is mid-swap`, () => {
|
|
39
55
|
// jsonFile writes `.<name>.<pid>.tmp` beside the target precisely so the atomic rename can't be read as
|
|
40
56
|
// a write to the target itself. A trailing-tag temp would prefix-match and bill an extra refetch.
|
|
41
57
|
expect(staleQueryKeys([`.intentic/.settings.json.42.tmp`], [])).toEqual([]);
|
|
42
|
-
expect(staleQueryKeys([`.intentic/settings.json`], [])).toEqual([`settings`]);
|
|
58
|
+
expect(staleQueryKeys([`.intentic/settings.json`], [])).toEqual([`settings`, `manifests`]);
|
|
43
59
|
});
|
|
44
60
|
|
|
45
61
|
it(`ignores ordinary workspace edits`, () => {
|
|
@@ -52,6 +68,7 @@ describe(`staleQueryKeys`, () => {
|
|
|
52
68
|
`capabilities`,
|
|
53
69
|
`environment`,
|
|
54
70
|
`panels`,
|
|
71
|
+
`manifests`,
|
|
55
72
|
]);
|
|
56
73
|
});
|
|
57
74
|
|
|
@@ -71,21 +88,53 @@ describe(`staleQueryKeys`, () => {
|
|
|
71
88
|
// The two lists are unioned flat, not layered: a narrow extension entry under a broad core entry that
|
|
72
89
|
// invalidates nothing must still fire. Without this, every path beneath one of the daemon's
|
|
73
90
|
// machine-state prefixes would be unreachable to extensions.
|
|
74
|
-
const nested: readonly FileContribution[] = [{ path:
|
|
75
|
-
expect(staleQueryKeys([`.intentic/claude/projects/p/memory/note.md`], nested)).toEqual([`memory`]);
|
|
91
|
+
const nested: readonly FileContribution[] = [{ path: `${STATE_DIR}/sessions/claude/projects/p/memory/`, invalidates: [`memory`] }];
|
|
92
|
+
expect(staleQueryKeys([`.intentic/sessions/claude/projects/p/memory/note.md`], nested)).toEqual([`memory`]);
|
|
76
93
|
// …and a sibling under the same core prefix stays ignored.
|
|
77
|
-
expect(staleQueryKeys([`.intentic/claude/projects/p/session.jsonl`], nested)).toEqual([]);
|
|
94
|
+
expect(staleQueryKeys([`.intentic/sessions/claude/projects/p/session.jsonl`], nested)).toEqual([]);
|
|
78
95
|
});
|
|
79
96
|
|
|
80
97
|
it(`dedupes a key two extensions both claim`, () => {
|
|
81
98
|
const twice: readonly FileContribution[] = [
|
|
82
|
-
{ path:
|
|
83
|
-
{ path:
|
|
99
|
+
{ path: `${STATE_DIR}/automations.json`, invalidates: [`automations`] },
|
|
100
|
+
{ path: `${STATE_DIR}/automations.json`, invalidates: [`automations`] },
|
|
84
101
|
];
|
|
85
102
|
expect(staleQueryKeys([`.intentic/automations.json`], twice)).toEqual([`automations`]);
|
|
86
103
|
});
|
|
87
104
|
});
|
|
88
105
|
|
|
106
|
+
/* Which broken manifests the owner is actually told about. The pair below is the whole invariant: a file is on
|
|
107
|
+
* the card iff a write to it refreshes the card. */
|
|
108
|
+
describe(`isReportedManifest`, () => {
|
|
109
|
+
it(`shows exactly the files whose writes refresh the notice`, () => {
|
|
110
|
+
expect(REPORTED_MANIFEST_PATHS.toSorted()).toEqual(
|
|
111
|
+
WORKSPACE_STATE_FILES.filter((file) => file.invalidates.includes(`manifests`))
|
|
112
|
+
.map((file) => file.path)
|
|
113
|
+
.toSorted(),
|
|
114
|
+
);
|
|
115
|
+
expect(isReportedManifest(`.intentic/settings.json`)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it(`stays quiet about daemon-written state the owner cannot repair`, () => {
|
|
119
|
+
// The regression this exists for: the workflow ledger's records predated a schema that gained a required
|
|
120
|
+
// field, so every read reported the whole file ignored — advice ("fix the file") addressed to nobody,
|
|
121
|
+
// about sixty kilobytes of machine JSON, and refreshed by no write because the ledger feeds no query, so
|
|
122
|
+
// it sat on the card until the daemon restarted. A ledger recovers on its own next write instead.
|
|
123
|
+
expect(isReportedManifest(`.intentic/workflow-runs.json`)).toBe(false);
|
|
124
|
+
expect(isReportedManifest(`.intentic/loops.json`)).toBe(false);
|
|
125
|
+
expect(isReportedManifest(`.intentic/thread-sessions.json`)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it(`reads a platform path with either separator`, () => {
|
|
129
|
+
expect(isReportedManifest(`.intentic\\settings.json`)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it(`does not report a file outside the workspace`, () => {
|
|
133
|
+
// What `relative` hands the daemon for the manifests it keeps under /history — never nameable on screen.
|
|
134
|
+
expect(isReportedManifest(`../../history/settings.json`)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
89
138
|
describe(`WORKSPACE_STATE_FILES`, () => {
|
|
90
139
|
it(`declares every entry under .intentic/, root-relative and forward-slash`, () => {
|
|
91
140
|
for (const file of WORKSPACE_STATE_FILES) {
|
|
@@ -117,13 +166,136 @@ describe(`WORKSPACE_STATE_FILES`, () => {
|
|
|
117
166
|
}
|
|
118
167
|
});
|
|
119
168
|
|
|
120
|
-
it(`
|
|
121
|
-
|
|
122
|
-
|
|
169
|
+
it(`only nests under an entry that invalidates nothing, so one write can't be billed twice`, () => {
|
|
170
|
+
// Entries may nest when a subtree needs a different portability class — stateFileFor's longest match
|
|
171
|
+
// keeps that unambiguous. Invalidation has no longest-match rule: staleQueryKeys unions every
|
|
172
|
+
// matching entry, so a nest under an entry that DOES invalidate would bill the outer view's queries for
|
|
173
|
+
// a write that belongs to the inner one. Nesting is therefore only legal beneath an empty `invalidates`.
|
|
174
|
+
for (const file of WORKSPACE_STATE_FILES.filter((entry) => entry.invalidates.length > 0)) {
|
|
175
|
+
const nested = WORKSPACE_STATE_FILES.filter((other) => other !== file && other.path.startsWith(file.path));
|
|
123
176
|
expect(
|
|
124
|
-
|
|
125
|
-
`${file.path} is a prefix of another entry`,
|
|
177
|
+
nested.map((other) => other.path),
|
|
178
|
+
`${file.path} invalidates ${file.invalidates.join(`, `)} and is a prefix of another entry`,
|
|
126
179
|
).toEqual([]);
|
|
127
180
|
}
|
|
128
181
|
});
|
|
182
|
+
|
|
183
|
+
it(`splits a nested entry from its parent for a reason the parent doesn't already carry`, () => {
|
|
184
|
+
// A nest that agrees with the entry it sits under is a duplicate: stateFileFor resolves to the same
|
|
185
|
+
// answer either way, so the split is dead weight the next reader has to diff to discover.
|
|
186
|
+
for (const file of WORKSPACE_STATE_FILES) {
|
|
187
|
+
for (const parent of WORKSPACE_STATE_FILES.filter((other) => other !== file && file.path.startsWith(other.path))) {
|
|
188
|
+
expect(
|
|
189
|
+
parent.portability === file.portability && parent.invalidates.join() === file.invalidates.join(),
|
|
190
|
+
`${file.path} says nothing its parent ${parent.path} doesn't already say`,
|
|
191
|
+
).toBe(false);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/* The rule the daemon enforces and the explorer draws. Both sides read it from here, which is the point: the
|
|
198
|
+
* padlock on a row and the refusal behind it can no longer disagree. */
|
|
199
|
+
describe(`isLockedWorkspacePath`, () => {
|
|
200
|
+
it(`covers the root state dir's credential entries, and their subtrees whole`, () => {
|
|
201
|
+
expect(isLockedWorkspacePath(`.intentic/capabilities.json`)).toBe(true);
|
|
202
|
+
expect(isLockedWorkspacePath(`.intentic/owner.json`)).toBe(true);
|
|
203
|
+
// A whole lifecycle root, so a provider added under it is covered without a second edit.
|
|
204
|
+
expect(isLockedWorkspacePath(`.intentic/auth`)).toBe(true);
|
|
205
|
+
expect(isLockedWorkspacePath(`.intentic/auth/codex/auth.json`)).toBe(true);
|
|
206
|
+
expect(isLockedWorkspacePath(`.intentic/browser/Default/Cookies`)).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it(`leaves the state dir's ordinary manifests alone`, () => {
|
|
210
|
+
// The dir itself is browsable, and most of what is in it is a file a person may legitimately read.
|
|
211
|
+
expect(isLockedWorkspacePath(`.intentic`)).toBe(false);
|
|
212
|
+
expect(isLockedWorkspacePath(`.intentic/settings.json`)).toBe(false);
|
|
213
|
+
expect(isLockedWorkspacePath(`.intentic/drafts/post-1.json`)).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it(`locks the ROOT's own .git and nobody else's`, () => {
|
|
217
|
+
// The root's is the pointer to the shadow history repo, kept where the agent cannot rewrite it; a
|
|
218
|
+
// repo's own .git is ordinary content that stays browsable.
|
|
219
|
+
expect(isLockedWorkspacePath(`.git`)).toBe(true);
|
|
220
|
+
expect(isLockedWorkspacePath(`.git/config`)).toBe(true);
|
|
221
|
+
expect(isLockedWorkspacePath(`myrepo/.git/config`)).toBe(false);
|
|
222
|
+
// …and a repo's own nested state dir is its project's, not the daemon's.
|
|
223
|
+
expect(isLockedWorkspacePath(`myrepo/.intentic/capabilities.json`)).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it(`reads a platform path the same as a posix one`, () => {
|
|
227
|
+
expect(isLockedWorkspacePath(`.intentic\\auth\\codex`)).toBe(true);
|
|
228
|
+
expect(isLockedWorkspacePath(`./.intentic/capabilities.json`)).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe(`VERSIONED_STATE_PATHS`, () => {
|
|
233
|
+
/* THE ONE ASSERTION THAT MUST NEVER GO GREEN BY ACCIDENT.
|
|
234
|
+
*
|
|
235
|
+
* `versioned` carves an entry out of the root repo's wholesale `.intentic` exclusion, so marking one is the
|
|
236
|
+
* difference between a file the owner reviews and a file the next baseline commit publishes into `git log`
|
|
237
|
+
* forever. A credential marked by a hurried hand is not recoverable by unmarking it later — the commit is
|
|
238
|
+
* already written — which is why the refusal is mechanical here rather than a rule in a comment. */
|
|
239
|
+
it(`never tracks a credential or an identity binding`, () => {
|
|
240
|
+
const leaked = WORKSPACE_STATE_FILES.filter((file) => file.versioned && (file.portability === `secret` || file.portability === `identity`));
|
|
241
|
+
expect(leaked.map((file) => file.path)).toEqual([]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
/* Narrower than `carry` ON PURPOSE, and this is where that stays true. The two answer different questions —
|
|
245
|
+
* carry is "does it move to a new sandbox", versioned is "should a human review it changing" — so the
|
|
246
|
+
* ledgers and the bulk are `carry` and deliberately absent: the run ledger is rewritten several times per
|
|
247
|
+
* workflow step, the usage batch every few seconds a browser is open, and the transcripts run to hundreds of
|
|
248
|
+
* megabytes. Tracking any of them buries the owner's code review under machine noise. */
|
|
249
|
+
it(`leaves the ledgers and the bulk out even though they travel`, () => {
|
|
250
|
+
for (const path of [
|
|
251
|
+
`.intentic/workflow-runs.json`,
|
|
252
|
+
// Split out of the tracked automations manifest precisely so a fire stops dirtying it — the one
|
|
253
|
+
// entry here that would be a REGRESSION rather than an oversight if it ever went tracked.
|
|
254
|
+
`.intentic/automation-runs.json`,
|
|
255
|
+
`.intentic/loops.json`,
|
|
256
|
+
`.intentic/thread-sessions.json`,
|
|
257
|
+
`.intentic/extension-usage.json`,
|
|
258
|
+
`.intentic/webchat-installs.json`,
|
|
259
|
+
`.intentic/sessions/claude/`,
|
|
260
|
+
`.intentic/artifacts/`,
|
|
261
|
+
]) {
|
|
262
|
+
expect([path, VERSIONED_STATE_PATHS.includes(path)]).toEqual([path, false]);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
/* Spelled out rather than derived, so ADDING a tracked entry is a visible edit to this list and not a silent
|
|
267
|
+
* consequence of editing the table above — the review the flag itself exists to force. */
|
|
268
|
+
it(`tracks exactly the configuration slice`, () => {
|
|
269
|
+
expect(VERSIONED_STATE_PATHS.toSorted()).toEqual([
|
|
270
|
+
`.intentic/automations.json`,
|
|
271
|
+
`.intentic/automations.seeded.json`,
|
|
272
|
+
`.intentic/capability-dismissals.json`,
|
|
273
|
+
`.intentic/environment.Dockerfile`,
|
|
274
|
+
`.intentic/environment.custom.Dockerfile`,
|
|
275
|
+
`.intentic/environment.d/`,
|
|
276
|
+
`.intentic/extension-enablement.json`,
|
|
277
|
+
// The owner's per-extension update posture (notify / agent / auto) — a standing decision about
|
|
278
|
+
// what may run unattended, which is exactly the kind of edit worth a line in `git log`.
|
|
279
|
+
`.intentic/extension-update-policy.json`,
|
|
280
|
+
`.intentic/loop-designs.json`,
|
|
281
|
+
`.intentic/personas.json`,
|
|
282
|
+
// A persona's own kit — the prompt it runs on and the skills only its turns reach. Tracked for the
|
|
283
|
+
// reason its card is, one step further: this is the text that decides how that persona behaves.
|
|
284
|
+
`.intentic/personas/`,
|
|
285
|
+
`.intentic/settings.json`,
|
|
286
|
+
// The skills the owner wrote. Tracked for the reason the rules in settings.json are: text that
|
|
287
|
+
// changes how the agent behaves is worth a diff and a line in `git log`.
|
|
288
|
+
`.intentic/skills/`,
|
|
289
|
+
`.intentic/templates.json`,
|
|
290
|
+
`.intentic/workflows.json`,
|
|
291
|
+
]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/* The composed overlay is `derived` — recomposed on every boot from the custom file that IS tracked, against
|
|
295
|
+
* whatever base image this container happens to be on. Tracking it would put a rewritten-at-startup file in
|
|
296
|
+
* front of the owner as a change they made. */
|
|
297
|
+
it(`tracks the environment overlay's source but not its composed output`, () => {
|
|
298
|
+
expect(VERSIONED_STATE_PATHS).toContain(`.intentic/environment.custom.Dockerfile`);
|
|
299
|
+
expect(VERSIONED_STATE_PATHS).not.toContain(`.intentic/environment.approved.Dockerfile`);
|
|
300
|
+
});
|
|
129
301
|
});
|