@selesai/code 0.13.12 → 0.13.14
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/CHANGELOG.md +9 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
- package/dist/extensions/pi-subagents/VISION.md +12 -1
- package/dist/extensions/pi-subagents/docs/agents.md +13 -6
- package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
- package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
- package/dist/extensions/pi-subagents/docs/missions.md +1 -1
- package/dist/extensions/pi-subagents/docs/models.md +6 -6
- package/dist/extensions/pi-subagents/docs/observability.md +6 -3
- package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
- package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/install.mjs +1 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
- package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
- package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
- package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
- package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
- package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
- package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
- package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
- package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
- package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
- package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
- package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
- package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
- package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
- package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
- package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
- package/dist/extensions/pi-zentui/README.md +19 -5
- package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
- package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
- package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
- package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
- package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
- package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
- package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
- package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
- package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
- package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
- package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
- package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
- package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
- package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
- package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
- package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
- package/dist/skills/brandkit/SKILL.md +0 -2
- package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
- package/dist/skills/gpt-taste/SKILL.md +0 -2
- package/dist/skills/image-to-code/SKILL.md +0 -2
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
- package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
- package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
- package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
- package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
- package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
- package/dist/skills/stitch/DESIGN.md +121 -0
- package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
- package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
- package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
- package/package.json +1 -1
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
aggregateAcceptanceReport,
|
|
12
12
|
evaluateAcceptance,
|
|
13
13
|
formatAcceptancePrompt,
|
|
14
|
+
normalizeAcceptanceInput,
|
|
14
15
|
normalizeGateAcceptance,
|
|
15
16
|
parseAcceptanceReport,
|
|
16
17
|
quoteExecutableForShell,
|
|
18
|
+
resolveAcceptanceReportMode,
|
|
17
19
|
resolveEffectiveAcceptance,
|
|
18
20
|
stripAcceptanceReport,
|
|
19
21
|
validateAcceptanceInput,
|
|
@@ -62,7 +64,7 @@ function tempGitRepo(): string {
|
|
|
62
64
|
|
|
63
65
|
describe("acceptance gates", () => {
|
|
64
66
|
it("infers evidence levels and review requirements independently", () => {
|
|
65
|
-
assert.equal(resolveEffectiveAcceptance({ agentName: "reviewer", task: "Review-only. Do not edit.", mode: "single" }).level, "
|
|
67
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "reviewer", task: "Review-only. Do not edit.", mode: "single" }).level, "none");
|
|
66
68
|
assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single" }).level, "checked");
|
|
67
69
|
for (const resolved of [
|
|
68
70
|
resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single", async: true }),
|
|
@@ -73,7 +75,7 @@ describe("acceptance gates", () => {
|
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
77
|
|
|
76
|
-
it("
|
|
78
|
+
it("omits inferred acceptance for async oracle review tasks despite implementation vocabulary", () => {
|
|
77
79
|
const resolved = resolveEffectiveAcceptance({
|
|
78
80
|
agentName: "oracle",
|
|
79
81
|
task: "Review prep findings and determine what to implement with playbooks instead of before.",
|
|
@@ -81,9 +83,9 @@ describe("acceptance gates", () => {
|
|
|
81
83
|
async: true,
|
|
82
84
|
});
|
|
83
85
|
|
|
84
|
-
assert.equal(resolved.level, "
|
|
86
|
+
assert.equal(resolved.level, "none");
|
|
85
87
|
assert.deepEqual(resolved.inferredReason, ["read-only/reviewer-style agent"]);
|
|
86
|
-
assert.deepEqual(resolved.criteria
|
|
88
|
+
assert.deepEqual(resolved.criteria, []);
|
|
87
89
|
});
|
|
88
90
|
|
|
89
91
|
it("uses explicit agent roles for ambiguous tasks while preserving task-intent precedence", () => {
|
|
@@ -92,7 +94,7 @@ describe("acceptance gates", () => {
|
|
|
92
94
|
acceptanceRole: "read-only",
|
|
93
95
|
task: "Explore the authentication flow",
|
|
94
96
|
mode: "single",
|
|
95
|
-
}).level, "
|
|
97
|
+
}).level, "none");
|
|
96
98
|
assert.equal(resolveEffectiveAcceptance({
|
|
97
99
|
agentName: "reviewer",
|
|
98
100
|
acceptanceRole: "writer",
|
|
@@ -119,13 +121,13 @@ describe("acceptance gates", () => {
|
|
|
119
121
|
acceptanceRole: "read-only",
|
|
120
122
|
task: "Create a report",
|
|
121
123
|
mode: "single",
|
|
122
|
-
}).level, "
|
|
124
|
+
}).level, "none");
|
|
123
125
|
assert.equal(resolveEffectiveAcceptance({
|
|
124
126
|
agentName: "worker",
|
|
125
127
|
acceptanceRole: "writer",
|
|
126
128
|
task: "Review only; do not edit files",
|
|
127
129
|
mode: "single",
|
|
128
|
-
}).level, "
|
|
130
|
+
}).level, "none");
|
|
129
131
|
assert.equal(resolveEffectiveAcceptance({
|
|
130
132
|
agentName: "reviewer",
|
|
131
133
|
acceptanceRole: "writer",
|
|
@@ -138,35 +140,35 @@ describe("acceptance gates", () => {
|
|
|
138
140
|
acceptanceRole: "read-only",
|
|
139
141
|
task: "Explore the authentication flow",
|
|
140
142
|
mode: "single",
|
|
141
|
-
}).level, "
|
|
143
|
+
}).level, "none");
|
|
142
144
|
assert.equal(resolveEffectiveAcceptance({
|
|
143
145
|
agentName: "explorer",
|
|
144
146
|
acceptanceRole: "read-only",
|
|
145
147
|
task: "Audit the security posture",
|
|
146
148
|
mode: "single",
|
|
147
|
-
}).level, "
|
|
149
|
+
}).level, "none");
|
|
148
150
|
assert.equal(resolveEffectiveAcceptance({
|
|
149
151
|
agentName: "explorer",
|
|
150
152
|
acceptanceRole: "read-only",
|
|
151
153
|
task: "Explore each target",
|
|
152
154
|
mode: "chain",
|
|
153
155
|
dynamic: true,
|
|
154
|
-
}).level, "
|
|
156
|
+
}).level, "none");
|
|
155
157
|
assert.equal(resolveEffectiveAcceptance({
|
|
156
158
|
agentName: "worker",
|
|
157
159
|
acceptanceRole: "writer",
|
|
158
160
|
task: "Review only; do not edit files",
|
|
159
161
|
mode: "chain",
|
|
160
162
|
dynamicGroup: true,
|
|
161
|
-
}).level, "
|
|
163
|
+
}).level, "none");
|
|
162
164
|
const dynamicReviewer = resolveEffectiveAcceptance({
|
|
163
165
|
agentName: "reviewer",
|
|
164
166
|
task: "Review each target",
|
|
165
167
|
mode: "chain",
|
|
166
168
|
dynamic: true,
|
|
167
169
|
});
|
|
168
|
-
assert.equal(dynamicReviewer.level, "
|
|
169
|
-
assert.equal(dynamicReviewer
|
|
170
|
+
assert.equal(dynamicReviewer.level, "none");
|
|
171
|
+
assert.equal(formatAcceptancePrompt(dynamicReviewer), "");
|
|
170
172
|
});
|
|
171
173
|
|
|
172
174
|
it("preserves risky keyword review inference when acceptance role metadata is omitted", () => {
|
|
@@ -254,6 +256,30 @@ describe("acceptance gates", () => {
|
|
|
254
256
|
assert.match(prompt, /"reviewFindings": \[\n "blocker:/);
|
|
255
257
|
});
|
|
256
258
|
|
|
259
|
+
it("omits inferred read-only prompts while preserving explicit acceptance", () => {
|
|
260
|
+
const inferred = resolveEffectiveAcceptance({
|
|
261
|
+
agentName: "reviewer",
|
|
262
|
+
task: "Review the diff and return findings only.",
|
|
263
|
+
});
|
|
264
|
+
assert.equal(inferred.level, "none");
|
|
265
|
+
assert.equal(formatAcceptancePrompt(inferred), "");
|
|
266
|
+
|
|
267
|
+
const explicit = resolveEffectiveAcceptance({
|
|
268
|
+
agentName: "reviewer",
|
|
269
|
+
task: "Review the diff and return findings only.",
|
|
270
|
+
explicit: { level: "checked", criteria: ["Return the review findings"] },
|
|
271
|
+
});
|
|
272
|
+
assert.match(formatAcceptancePrompt(explicit), /```acceptance-report/);
|
|
273
|
+
|
|
274
|
+
const explicitCriteria = resolveEffectiveAcceptance({
|
|
275
|
+
agentName: "reviewer",
|
|
276
|
+
task: "Review the diff and return findings only.",
|
|
277
|
+
explicit: { criteria: ["Return the review findings"] },
|
|
278
|
+
});
|
|
279
|
+
assert.equal(explicitCriteria.level, "attested");
|
|
280
|
+
assert.match(formatAcceptancePrompt(explicitCriteria), /```acceptance-report/);
|
|
281
|
+
});
|
|
282
|
+
|
|
257
283
|
it("includes every required resolved criterion in report examples", () => {
|
|
258
284
|
const inferred = resolveEffectiveAcceptance({ agentName: "worker", task: "Implement the fix", mode: "single", async: true });
|
|
259
285
|
const inferredExample = formatAcceptancePrompt(inferred).match(/```acceptance-report\n([\s\S]*?)\n```/);
|
|
@@ -774,6 +800,15 @@ describe("acceptance gates", () => {
|
|
|
774
800
|
});
|
|
775
801
|
assert.equal(malformedDirect.status, "rejected");
|
|
776
802
|
assert.match(malformedDirect.childReportParseError ?? "", /unexpected: unsupported acceptance report field/);
|
|
803
|
+
|
|
804
|
+
const nullDirect = await evaluateAcceptance({
|
|
805
|
+
acceptance,
|
|
806
|
+
output: report({ changedFiles: [], testsAddedOrUpdated: [] }),
|
|
807
|
+
report: null as never,
|
|
808
|
+
cwd,
|
|
809
|
+
});
|
|
810
|
+
assert.equal(nullDirect.status, "rejected");
|
|
811
|
+
assert.match(nullDirect.childReportParseError ?? "", /acceptance-report: expected object; got null/);
|
|
777
812
|
} finally {
|
|
778
813
|
fs.rmSync(cwd, { recursive: true, force: true });
|
|
779
814
|
}
|
|
@@ -1185,20 +1220,45 @@ describe("acceptance gates", () => {
|
|
|
1185
1220
|
assert.match(errors.join("\n"), /acceptance\.review\.required/);
|
|
1186
1221
|
});
|
|
1187
1222
|
|
|
1223
|
+
it("requires outputSchema for explicit structured acceptance report mode", () => {
|
|
1224
|
+
const schema = { type: "object" as const };
|
|
1225
|
+
const errors = validateExecutionAcceptance({
|
|
1226
|
+
acceptance: { level: "checked", report: "on" },
|
|
1227
|
+
tasks: [
|
|
1228
|
+
{ acceptance: { level: "checked", report: "off" } },
|
|
1229
|
+
{ outputSchema: schema, acceptance: { level: "checked", report: "on" } },
|
|
1230
|
+
],
|
|
1231
|
+
chain: [
|
|
1232
|
+
{ acceptance: { level: "checked", report: "on" } },
|
|
1233
|
+
{ outputSchema: schema, acceptance: { level: "checked", report: "off" } },
|
|
1234
|
+
{ parallel: [{ acceptance: { level: "checked", report: "on" } }, { outputSchema: schema, acceptance: { level: "checked", report: "on" } }] },
|
|
1235
|
+
{ parallel: { acceptance: { level: "checked", report: "off" } } },
|
|
1236
|
+
],
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
assert.deepEqual(errors, [
|
|
1240
|
+
"acceptance.report requires outputSchema.",
|
|
1241
|
+
"tasks[0].acceptance.report requires outputSchema.",
|
|
1242
|
+
"chain[0].acceptance.report requires outputSchema.",
|
|
1243
|
+
"chain[2].parallel[0].acceptance.report requires outputSchema.",
|
|
1244
|
+
"chain[3].parallel.acceptance.report requires outputSchema.",
|
|
1245
|
+
]);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1188
1248
|
it("blanket read-only wording is not inferred as a risky write task", () => {
|
|
1189
1249
|
const readOnlyWorker = resolveEffectiveAcceptance({
|
|
1190
1250
|
agentName: "worker",
|
|
1191
1251
|
task: "Report on the extraction pipeline. Do not modify project/source files.",
|
|
1192
1252
|
async: true,
|
|
1193
1253
|
});
|
|
1194
|
-
assert.equal(readOnlyWorker.level, "
|
|
1254
|
+
assert.equal(readOnlyWorker.level, "none");
|
|
1195
1255
|
for (const task of [
|
|
1196
1256
|
"Inspect the extraction pipeline",
|
|
1197
1257
|
"Summarize the extraction pipeline",
|
|
1198
1258
|
"Review only: return findings",
|
|
1199
1259
|
"Analyze the extraction pipeline without edits",
|
|
1200
1260
|
]) {
|
|
1201
|
-
assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task, async: true }).level, "
|
|
1261
|
+
assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task, async: true }).level, "none", task);
|
|
1202
1262
|
}
|
|
1203
1263
|
|
|
1204
1264
|
assert.equal(resolveEffectiveAcceptance({ agentName: "worker", task: "Inspect the failure and implement the fix" }).level, "checked");
|
|
@@ -1332,6 +1392,17 @@ describe("acceptance gates", () => {
|
|
|
1332
1392
|
}
|
|
1333
1393
|
});
|
|
1334
1394
|
|
|
1395
|
+
it("normalizes JSON-encoded acceptance objects before existing validation", () => {
|
|
1396
|
+
const inline = { level: "checked" as const, evidence: ["commands-run" as const, "changed-files" as const] };
|
|
1397
|
+
const encoded = JSON.stringify(inline);
|
|
1398
|
+
|
|
1399
|
+
assert.deepEqual(normalizeAcceptanceInput(encoded), inline);
|
|
1400
|
+
assert.deepEqual(validateAcceptanceInput(encoded), validateAcceptanceInput(inline));
|
|
1401
|
+
assert.match(validateAcceptanceInput(JSON.stringify({ evidence: "commands-run" })).join("\n"), /acceptance\.evidence must be an array/);
|
|
1402
|
+
assert.match(validateAcceptanceInput('{"level":"checked"').join("\n"), /acceptance JSON string must encode a valid acceptance object/i);
|
|
1403
|
+
assert.match(validateAcceptanceInput(JSON.stringify(["checked"])).join("\n"), /acceptance JSON string must encode an object/i);
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1335
1406
|
it("validates invalid disable and verify shapes", () => {
|
|
1336
1407
|
assert.deepEqual(validateAcceptanceInput({ level: "none" }), ["acceptance.reason is required when level is none."]);
|
|
1337
1408
|
assert.deepEqual(validateAcceptanceInput("none"), ["acceptance level \"none\" requires a reason; use { level: \"none\", reason: \"...\" }."]);
|
|
@@ -1342,6 +1413,8 @@ describe("acceptance gates", () => {
|
|
|
1342
1413
|
assert.deepEqual(validateAcceptanceInput({ level: "verified", verify: [{ id: "tests", command: "npm test" }] }), []);
|
|
1343
1414
|
assert.deepEqual(validateAcceptanceInput({ level: "verified", verify: [{ id: "tests", command: "npm test" }, { id: "lint", command: "npm run lint" }] }), []);
|
|
1344
1415
|
assert.deepEqual(validateAcceptanceInput({ level: "checked" }), []);
|
|
1416
|
+
assert.deepEqual(validateAcceptanceInput({ level: "checked", report: "on" }), []);
|
|
1417
|
+
assert.match(validateAcceptanceInput({ report: "sometimes" }).join("\n"), /acceptance\.report must be on or off/);
|
|
1345
1418
|
assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "missing-command" }] }), ["acceptance.verify[0].command is required."]);
|
|
1346
1419
|
assert.deepEqual(validateAcceptanceInput({ verify: [{ id: "fractional", command: "npm test", timeoutMs: 1.5 }] }), ["acceptance.verify[0].timeoutMs must be an integer >= 1."]);
|
|
1347
1420
|
assert.deepEqual(validateAcceptanceInput(false), []);
|
|
@@ -1363,6 +1436,14 @@ describe("acceptance gates", () => {
|
|
|
1363
1436
|
assert.match(validateAcceptanceInput({ stopRules: [123] }).join("\n"), /acceptance\.stopRules\[0\] must be a string/);
|
|
1364
1437
|
assert.match(validateAcceptanceInput({ surprise: true }).join("\n"), /acceptance\.surprise is not supported/);
|
|
1365
1438
|
});
|
|
1439
|
+
|
|
1440
|
+
it("resolves structured acceptance report capture without changing the omitted default", () => {
|
|
1441
|
+
assert.equal(resolveAcceptanceReportMode(undefined), "optional");
|
|
1442
|
+
assert.equal(resolveAcceptanceReportMode("checked"), "optional");
|
|
1443
|
+
assert.equal(resolveAcceptanceReportMode(false), "off");
|
|
1444
|
+
assert.equal(resolveAcceptanceReportMode({ report: "off" }), "off");
|
|
1445
|
+
assert.equal(resolveAcceptanceReportMode({ report: "on" }), "required");
|
|
1446
|
+
});
|
|
1366
1447
|
});
|
|
1367
1448
|
|
|
1368
1449
|
describe("quoteExecutableForShell", () => {
|
|
@@ -5,6 +5,7 @@ import * as path from "node:path";
|
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import {
|
|
7
7
|
acquireActiveAsyncCapacity,
|
|
8
|
+
activeAsyncCapacitySessionKey,
|
|
8
9
|
ActiveAsyncCapacityError,
|
|
9
10
|
getActiveAsyncCapacitySnapshot,
|
|
10
11
|
inspectActiveAsyncCapacityOwner,
|
|
@@ -194,7 +195,7 @@ describe("active async capacity", () => {
|
|
|
194
195
|
}
|
|
195
196
|
});
|
|
196
197
|
|
|
197
|
-
it("
|
|
198
|
+
it("restores a transferred source when revival fails before runner proceed", () => {
|
|
198
199
|
const rootDir = tempRoot();
|
|
199
200
|
const sourceDir = path.join(rootDir, "runs", "source");
|
|
200
201
|
const failedDir = path.join(rootDir, "runs", "failed-revival");
|
|
@@ -206,8 +207,55 @@ describe("active async capacity", () => {
|
|
|
206
207
|
const transferred = transferActiveAsyncCapacity({ sessionId: "session-a", limit: 1, sourceRunId: "source", runId: "failed-revival", asyncDir: failedDir }, { rootDir });
|
|
207
208
|
assert.ok(transferred);
|
|
208
209
|
transferred.markStarted("failed-runner");
|
|
209
|
-
|
|
210
|
+
assert.equal(transferred.rollbackBeforeRunnerProceed("failed-runner"), true);
|
|
210
211
|
|
|
212
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
213
|
+
const sourceInspection = inspectActiveAsyncCapacityOwner({ sessionId: "session-a", runId: "source", asyncDir: sourceDir }, { rootDir });
|
|
214
|
+
assert.equal(sourceInspection.relation, "current");
|
|
215
|
+
assert.equal(sourceInspection.owner?.runId, "source");
|
|
216
|
+
assert.equal(inspectActiveAsyncCapacityOwner({ sessionId: "session-a", runId: "failed-revival", asyncDir: failedDir }, { rootDir }).relation, "none");
|
|
217
|
+
} finally {
|
|
218
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("releases capacity bound before runner proceed when startup is terminated", () => {
|
|
223
|
+
const rootDir = tempRoot();
|
|
224
|
+
const unboundDir = path.join(rootDir, "runs", "pre-bind");
|
|
225
|
+
const asyncDir = path.join(rootDir, "runs", "pre-proceed");
|
|
226
|
+
try {
|
|
227
|
+
const unbound = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-bind", kind: "runner", asyncDir: unboundDir }, { rootDir });
|
|
228
|
+
assert.ok(unbound);
|
|
229
|
+
assert.equal(unbound.rollbackBeforeRunnerProceed("pre-bind-runner"), false);
|
|
230
|
+
assert.equal(unbound.rollback(), true);
|
|
231
|
+
|
|
232
|
+
const handle = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-proceed", kind: "runner", asyncDir }, { rootDir });
|
|
233
|
+
assert.ok(handle);
|
|
234
|
+
handle.markStarted("pre-proceed-runner");
|
|
235
|
+
assert.equal(handle.rollback(), false);
|
|
236
|
+
|
|
237
|
+
assert.equal(handle.rollbackBeforeRunnerProceed("other-runner"), false);
|
|
238
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
239
|
+
assert.equal(handle.rollbackBeforeRunnerProceed("pre-proceed-runner"), true);
|
|
240
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
241
|
+
|
|
242
|
+
const bindFailure = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-proceed-bind-failure", kind: "runner", asyncDir: path.join(rootDir, "runs", "pre-proceed-bind-failure") }, {
|
|
243
|
+
rootDir,
|
|
244
|
+
writeOwner() { throw new Error("simulated durable bind failure"); },
|
|
245
|
+
});
|
|
246
|
+
assert.ok(bindFailure);
|
|
247
|
+
assert.throws(() => bindFailure.markStarted("pre-proceed-bind-failure-runner"), /simulated durable bind failure/);
|
|
248
|
+
assert.equal(bindFailure.rollback(), false);
|
|
249
|
+
assert.equal(bindFailure.rollbackBeforeRunnerProceed("pre-proceed-bind-failure-runner"), true);
|
|
250
|
+
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
251
|
+
|
|
252
|
+
const divergent = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "pre-proceed-divergent", kind: "runner", asyncDir: path.join(rootDir, "runs", "pre-proceed-divergent") }, { rootDir });
|
|
253
|
+
assert.ok(divergent);
|
|
254
|
+
const unstartedOwner = { ...divergent.owner };
|
|
255
|
+
divergent.markStarted("pre-proceed-divergent-runner");
|
|
256
|
+
writeJson(path.join(rootDir, activeAsyncCapacitySessionKey("session-a"), "slot-0", "owner.json"), unstartedOwner);
|
|
257
|
+
assert.equal(divergent.rollback(), false);
|
|
258
|
+
assert.equal(divergent.rollbackBeforeRunnerProceed("pre-proceed-divergent-runner"), true);
|
|
211
259
|
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
212
260
|
} finally {
|
|
213
261
|
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -296,7 +344,7 @@ describe("active async capacity", () => {
|
|
|
296
344
|
}
|
|
297
345
|
});
|
|
298
346
|
|
|
299
|
-
it("releases terminal workflows
|
|
347
|
+
it("releases terminal workflows despite stale step projections after the controller and async children stop", () => {
|
|
300
348
|
const rootDir = tempRoot();
|
|
301
349
|
const asyncRoot = path.join(rootDir, "runs");
|
|
302
350
|
const workflowDir = path.join(asyncRoot, "workflow");
|
|
@@ -305,14 +353,14 @@ describe("active async capacity", () => {
|
|
|
305
353
|
const workflow = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow", kind: "workflow", asyncDir: workflowDir }, { rootDir });
|
|
306
354
|
assert.ok(workflow);
|
|
307
355
|
workflow.markWorkflowStarted();
|
|
308
|
-
writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "foreground", async: false, status: "
|
|
356
|
+
writeJson(path.join(workflowDir, "status.json"), { runId: "workflow", sessionId: "session-a", mode: "workflow", state: "complete", startedAt: 100, steps: [{ agent: "worker", workflowKey: "foreground", async: false, status: "running" }] });
|
|
309
357
|
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir, liveWorkflowRunIds: new Set(["workflow"]) }), { used: 1, limit: 1 });
|
|
310
358
|
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 0, limit: 1 });
|
|
311
359
|
|
|
312
360
|
const second = acquireActiveAsyncCapacity({ sessionId: "session-a", limit: 1, runId: "workflow-2", kind: "workflow", asyncDir: workflowDir }, { rootDir });
|
|
313
361
|
assert.ok(second);
|
|
314
362
|
second.markWorkflowStarted();
|
|
315
|
-
writeJson(path.join(workflowDir, "status.json"), { runId: "workflow-2", sessionId: "session-a", mode: "workflow", state: "
|
|
363
|
+
writeJson(path.join(workflowDir, "status.json"), { runId: "workflow-2", sessionId: "session-a", mode: "workflow", state: "failed", startedAt: 100, steps: [{ agent: "worker", workflowKey: "async", runId: "child", async: true, status: "running" }] });
|
|
316
364
|
writeJson(path.join(childDir, "status.json"), { runId: "child", sessionId: "session-a", mode: "single", state: "complete", startedAt: 100, processTerminal: { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" } });
|
|
317
365
|
writeJson(path.join(childDir, "process-terminal.json"), { version: 1, state: "unknown", runId: "child", runnerProcessInstanceId: "runner-child", reason: "process-tree-unverified" });
|
|
318
366
|
assert.deepEqual(getActiveAsyncCapacitySnapshot("session-a", 1, { rootDir }), { used: 1, limit: 1 });
|
|
@@ -515,6 +515,26 @@ Do work
|
|
|
515
515
|
assert.deepEqual(worker?.extensions, [path.join(dir, ".selesai", "agents", "extension-one.ts"), path.join(dir, ".selesai", "agents", "extension-two.ts")]);
|
|
516
516
|
assert.deepEqual(worker?.subagentOnlyExtensions, [path.join(dir, ".selesai", "agents", "child-only.ts"), path.join(dir, ".selesai", "agents", "child-helper.ts")]);
|
|
517
517
|
});
|
|
518
|
+
|
|
519
|
+
it("discovers and serializes excludeTools without validating names", () => {
|
|
520
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-exclude-tools-frontmatter-"));
|
|
521
|
+
tempDirs.push(dir);
|
|
522
|
+
const agentPath = path.join(dir, ".selesai", "agents", "worker.md");
|
|
523
|
+
writeAgent(agentPath, `---
|
|
524
|
+
name: worker
|
|
525
|
+
description: Worker
|
|
526
|
+
excludeTools:
|
|
527
|
+
- write
|
|
528
|
+
- made-up-tool
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
Do work
|
|
532
|
+
`);
|
|
533
|
+
|
|
534
|
+
const worker = discoverAgents(dir, "project").agents.find((agent) => agent.name === "worker");
|
|
535
|
+
assert.deepEqual(worker?.excludeTools, ["write", "made-up-tool"]);
|
|
536
|
+
assert.match(serializeAgent(worker!), /^excludeTools: write, made-up-tool$/m);
|
|
537
|
+
});
|
|
518
538
|
});
|
|
519
539
|
|
|
520
540
|
describe("agent permission frontmatter", () => {
|
|
@@ -8,6 +8,7 @@ import { EXTRA_AGENT_DIRS_ENV } from "../../src/agents/agents.ts";
|
|
|
8
8
|
import { EXTERNAL_JOB_PROVIDER_REGISTRY_KEY, registerExternalJobProvider } from "../../src/api/external-job-provider.ts";
|
|
9
9
|
import { clearSkillCache } from "../../src/agents/skills.ts";
|
|
10
10
|
import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../src/shared/utils.ts";
|
|
11
|
+
import { openSubagentsAdmin } from "../../src/slash/subagents-admin.ts";
|
|
11
12
|
|
|
12
13
|
let tempDir = "";
|
|
13
14
|
let oldAgentDir: string | undefined;
|
|
@@ -891,7 +892,219 @@ Drive the failing test first.
|
|
|
891
892
|
assert.match(afterText, /Inherit skills: true/);
|
|
892
893
|
});
|
|
893
894
|
|
|
894
|
-
it("
|
|
895
|
+
it("does not serialize settings descriptions, fast, or defaults into custom agent frontmatter during updates", () => {
|
|
896
|
+
const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
|
|
897
|
+
const settingsPath = path.join(tempDir, ".selesai", "settings.json");
|
|
898
|
+
const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
|
|
899
|
+
fs.mkdirSync(path.dirname(agentPath), { recursive: true });
|
|
900
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
901
|
+
subagents: {
|
|
902
|
+
defaultModel: "openai/gpt-default",
|
|
903
|
+
defaultThinking: "high",
|
|
904
|
+
agentOverrides: {
|
|
905
|
+
implementer: { description: "Settings description", output: "settings.md", fast: true },
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
}, null, 2), "utf-8");
|
|
909
|
+
fs.writeFileSync(agentPath, `---
|
|
910
|
+
name: implementer
|
|
911
|
+
description: Frontmatter description
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
Drive the failing test first.
|
|
915
|
+
`, "utf-8");
|
|
916
|
+
|
|
917
|
+
const beforeText = readText(handleManagementAction("get", { agent: "implementer" }, ctx));
|
|
918
|
+
assert.match(beforeText, /Description: Settings description/);
|
|
919
|
+
assert.match(beforeText, /Model: openai\/gpt-default/);
|
|
920
|
+
assert.match(beforeText, /Thinking: high/);
|
|
921
|
+
|
|
922
|
+
const updated = handleUpdate({ agent: "implementer", config: { output: "local.md" } }, ctx);
|
|
923
|
+
assert.equal(updated.isError, false);
|
|
924
|
+
|
|
925
|
+
const content = fs.readFileSync(agentPath, "utf-8");
|
|
926
|
+
assert.match(content, /^description: Frontmatter description$/m);
|
|
927
|
+
assert.match(content, /^output: local\.md$/m);
|
|
928
|
+
assert.doesNotMatch(content, /^fast:/m);
|
|
929
|
+
assert.doesNotMatch(content, /^model:/m);
|
|
930
|
+
assert.doesNotMatch(content, /^thinking:/m);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it("keeps same-value custom override ownership for interactive admin edits", async () => {
|
|
934
|
+
const settingsPath = path.join(tempDir, ".selesai", "settings.json");
|
|
935
|
+
const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
|
|
936
|
+
fs.mkdirSync(path.dirname(agentPath), { recursive: true });
|
|
937
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
938
|
+
subagents: {
|
|
939
|
+
agentOverrides: { implementer: { model: "anthropic/claude-old" } },
|
|
940
|
+
},
|
|
941
|
+
}, null, 2), "utf-8");
|
|
942
|
+
fs.writeFileSync(agentPath, `---
|
|
943
|
+
name: implementer
|
|
944
|
+
description: Frontmatter description
|
|
945
|
+
model: anthropic/claude-old
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
Drive the failing test first.
|
|
949
|
+
`, "utf-8");
|
|
950
|
+
const sent: unknown[] = [];
|
|
951
|
+
const notified: string[] = [];
|
|
952
|
+
|
|
953
|
+
await openSubagentsAdmin(
|
|
954
|
+
{ sendMessage: (message: unknown) => sent.push(message) } as never,
|
|
955
|
+
{
|
|
956
|
+
cwd: tempDir,
|
|
957
|
+
hasUI: true,
|
|
958
|
+
modelRegistry: {
|
|
959
|
+
getAvailable: () => [
|
|
960
|
+
{ provider: "anthropic", id: "claude-old" },
|
|
961
|
+
{ provider: "anthropic", id: "claude-new" },
|
|
962
|
+
],
|
|
963
|
+
},
|
|
964
|
+
ui: {
|
|
965
|
+
select: async () => "anthropic/claude-new",
|
|
966
|
+
notify: (message: string) => notified.push(message),
|
|
967
|
+
},
|
|
968
|
+
} as never,
|
|
969
|
+
"implementer model",
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
assert.match(notified[0] ?? "", /Saved project settings override/);
|
|
973
|
+
assert.match(JSON.stringify(sent), /Saved project settings override/);
|
|
974
|
+
assert.match(fs.readFileSync(agentPath, "utf-8"), /^model: anthropic\/claude-old$/m);
|
|
975
|
+
assert.equal(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-new");
|
|
976
|
+
const after = readText(handleManagementAction("get", { agent: "implementer" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
|
|
977
|
+
assert.match(after, /Model: anthropic\/claude-new/);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("promotes cross-scope custom override edits to project settings", async () => {
|
|
981
|
+
const userSettingsPath = path.join(tempDir, "agent-home", "settings.json");
|
|
982
|
+
const projectSettingsPath = path.join(tempDir, ".selesai", "settings.json");
|
|
983
|
+
const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
|
|
984
|
+
fs.mkdirSync(path.dirname(userSettingsPath), { recursive: true });
|
|
985
|
+
fs.mkdirSync(path.dirname(agentPath), { recursive: true });
|
|
986
|
+
fs.writeFileSync(userSettingsPath, JSON.stringify({
|
|
987
|
+
subagents: {
|
|
988
|
+
agentOverrides: {
|
|
989
|
+
implementer: { model: "anthropic/claude-old" },
|
|
990
|
+
critic: { thinking: "high" },
|
|
991
|
+
speaker: { systemPrompt: "Shared prompt" },
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
}, null, 2), "utf-8");
|
|
995
|
+
fs.writeFileSync(projectSettingsPath, JSON.stringify({
|
|
996
|
+
subagents: { defaultModel: "openai/gpt-default" },
|
|
997
|
+
}, null, 2), "utf-8");
|
|
998
|
+
fs.writeFileSync(agentPath, `---
|
|
999
|
+
name: implementer
|
|
1000
|
+
description: Frontmatter description
|
|
1001
|
+
---
|
|
1002
|
+
|
|
1003
|
+
Drive the failing test first.
|
|
1004
|
+
`, "utf-8");
|
|
1005
|
+
fs.writeFileSync(path.join(path.dirname(agentPath), "critic.md"), `---
|
|
1006
|
+
name: critic
|
|
1007
|
+
description: Thinking critic
|
|
1008
|
+
thinking: false
|
|
1009
|
+
---
|
|
1010
|
+
|
|
1011
|
+
Keep thinking off.
|
|
1012
|
+
`, "utf-8");
|
|
1013
|
+
fs.writeFileSync(path.join(path.dirname(agentPath), "speaker.md"), `---
|
|
1014
|
+
name: speaker
|
|
1015
|
+
description: Prompt speaker
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
Base prompt.
|
|
1019
|
+
`, "utf-8");
|
|
1020
|
+
const sent: unknown[] = [];
|
|
1021
|
+
const notified: string[] = [];
|
|
1022
|
+
|
|
1023
|
+
await openSubagentsAdmin(
|
|
1024
|
+
{ sendMessage: (message: unknown) => sent.push(message) } as never,
|
|
1025
|
+
{
|
|
1026
|
+
cwd: tempDir,
|
|
1027
|
+
hasUI: true,
|
|
1028
|
+
modelRegistry: {
|
|
1029
|
+
getAvailable: () => [
|
|
1030
|
+
{ provider: "anthropic", id: "claude-old" },
|
|
1031
|
+
{ provider: "anthropic", id: "claude-new" },
|
|
1032
|
+
],
|
|
1033
|
+
},
|
|
1034
|
+
ui: {
|
|
1035
|
+
select: async () => "anthropic/claude-new",
|
|
1036
|
+
notify: (message: string) => notified.push(message),
|
|
1037
|
+
},
|
|
1038
|
+
} as never,
|
|
1039
|
+
"implementer model",
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
assert.match(notified[0] ?? "", /Saved project settings override/);
|
|
1043
|
+
assert.match(JSON.stringify(sent), /Saved project settings override/);
|
|
1044
|
+
assert.doesNotMatch(fs.readFileSync(agentPath, "utf-8"), /^model:/m);
|
|
1045
|
+
assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-old");
|
|
1046
|
+
assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-new");
|
|
1047
|
+
const after = readText(handleManagementAction("get", { agent: "implementer" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
|
|
1048
|
+
assert.match(after, /Model: anthropic\/claude-new/);
|
|
1049
|
+
|
|
1050
|
+
await openSubagentsAdmin(
|
|
1051
|
+
{ sendMessage: (message: unknown) => sent.push(message) } as never,
|
|
1052
|
+
{
|
|
1053
|
+
cwd: tempDir,
|
|
1054
|
+
hasUI: true,
|
|
1055
|
+
modelRegistry: { getAvailable: () => [{ provider: "anthropic", id: "claude-new" }] },
|
|
1056
|
+
ui: {
|
|
1057
|
+
select: async () => "Default / inherit session model",
|
|
1058
|
+
notify: (message: string) => notified.push(message),
|
|
1059
|
+
},
|
|
1060
|
+
} as never,
|
|
1061
|
+
"implementer model",
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "anthropic/claude-old");
|
|
1065
|
+
assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.implementer.model, "openai/gpt-default");
|
|
1066
|
+
const cleared = readText(handleManagementAction("get", { agent: "implementer" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
|
|
1067
|
+
assert.match(cleared, /Model: openai\/gpt-default/);
|
|
1068
|
+
assert.doesNotMatch(cleared, /Model: anthropic\/claude-old|Model: anthropic\/claude-new/);
|
|
1069
|
+
|
|
1070
|
+
await openSubagentsAdmin(
|
|
1071
|
+
{ sendMessage: (message: unknown) => sent.push(message) } as never,
|
|
1072
|
+
{
|
|
1073
|
+
cwd: tempDir,
|
|
1074
|
+
hasUI: true,
|
|
1075
|
+
modelRegistry: { getAvailable: () => [] },
|
|
1076
|
+
ui: {
|
|
1077
|
+
select: async () => "Default / inherit session thinking",
|
|
1078
|
+
notify: (message: string) => notified.push(message),
|
|
1079
|
+
},
|
|
1080
|
+
} as never,
|
|
1081
|
+
"critic thinking",
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.critic.thinking, "high");
|
|
1085
|
+
assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.critic.thinking, "off");
|
|
1086
|
+
const thinkingCleared = readText(handleManagementAction("get", { agent: "critic" }, { cwd: tempDir, modelRegistry: { getAvailable: () => [] } }));
|
|
1087
|
+
assert.match(thinkingCleared, /Thinking: off/);
|
|
1088
|
+
|
|
1089
|
+
await openSubagentsAdmin(
|
|
1090
|
+
{ sendMessage: (message: unknown) => sent.push(message) } as never,
|
|
1091
|
+
{
|
|
1092
|
+
cwd: tempDir,
|
|
1093
|
+
hasUI: true,
|
|
1094
|
+
modelRegistry: { getAvailable: () => [] },
|
|
1095
|
+
ui: {
|
|
1096
|
+
editor: async () => "Shared prompt",
|
|
1097
|
+
notify: (message: string) => notified.push(message),
|
|
1098
|
+
},
|
|
1099
|
+
} as never,
|
|
1100
|
+
"speaker system-prompt",
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
assert.equal(JSON.parse(fs.readFileSync(userSettingsPath, "utf-8")).subagents.agentOverrides.speaker.systemPrompt, "Shared prompt");
|
|
1104
|
+
assert.equal(JSON.parse(fs.readFileSync(projectSettingsPath, "utf-8")).subagents.agentOverrides.speaker.systemPrompt, "Shared prompt");
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it("preserves blank output and defaultReads frontmatter while settings overrides replace them", () => {
|
|
895
1108
|
const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
|
|
896
1109
|
const settingsPath = path.join(tempDir, ".selesai", "settings.json");
|
|
897
1110
|
const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
|
|
@@ -916,11 +1129,11 @@ Drive the failing test first.
|
|
|
916
1129
|
assert.match(content, /^output: ?$/m);
|
|
917
1130
|
assert.match(content, /^defaultReads: ?$/m);
|
|
918
1131
|
const after = readText(handleManagementAction("get", { agent: "implementer" }, ctx));
|
|
919
|
-
assert.
|
|
920
|
-
assert.
|
|
1132
|
+
assert.match(after, /Output: settings\.md/);
|
|
1133
|
+
assert.match(after, /Reads: settings\.md/);
|
|
921
1134
|
});
|
|
922
1135
|
|
|
923
|
-
it("preserves explicit default-like frontmatter
|
|
1136
|
+
it("preserves explicit default-like frontmatter while settings overrides replace it", () => {
|
|
924
1137
|
const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
|
|
925
1138
|
const settingsPath = path.join(tempDir, ".selesai", "settings.json");
|
|
926
1139
|
const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
|
|
@@ -958,8 +1171,8 @@ Drive the failing test first.
|
|
|
958
1171
|
const got = handleManagementAction("get", { agent: "implementer" }, ctx);
|
|
959
1172
|
assert.equal(got.isError, false);
|
|
960
1173
|
const beforeText = readText(got);
|
|
961
|
-
assert.match(beforeText, /Thinking:
|
|
962
|
-
assert.doesNotMatch(beforeText, /Thinking:
|
|
1174
|
+
assert.match(beforeText, /Thinking: high/);
|
|
1175
|
+
assert.doesNotMatch(beforeText, /Thinking: off/);
|
|
963
1176
|
|
|
964
1177
|
const updated = handleUpdate(
|
|
965
1178
|
{ agent: "implementer", config: { description: "Updated implementer" } },
|
|
@@ -980,8 +1193,8 @@ Drive the failing test first.
|
|
|
980
1193
|
const gotAfter = handleManagementAction("get", { agent: "implementer" }, ctx);
|
|
981
1194
|
assert.equal(gotAfter.isError, false);
|
|
982
1195
|
const afterText = readText(gotAfter);
|
|
983
|
-
assert.match(afterText, /Thinking:
|
|
984
|
-
assert.doesNotMatch(afterText, /Thinking:
|
|
1196
|
+
assert.match(afterText, /Thinking: high/);
|
|
1197
|
+
assert.doesNotMatch(afterText, /Thinking: off/);
|
|
985
1198
|
});
|
|
986
1199
|
|
|
987
1200
|
it("reports builtin runtime-loaded model mappings from current session state", () => {
|