@xingular/harness 1.4.0-unstable → 1.5.0-unstable
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 +41 -1
- package/NOTICE.md +9 -7
- package/dist/cli.js +160 -456
- package/dist/sdk/prompt/compile.d.ts +4 -2
- package/dist/sdk/prompt/compile.js +10 -8
- package/dist/sdk/prompt/reporting.d.ts +2 -2
- package/dist/sdk/prompt/reporting.js +40 -250
- package/dist/sdk/prompt/standard.d.ts +4 -1
- package/dist/sdk/prompt/standard.js +6 -2
- package/dist/sdk/sessions/completionCheck.d.ts +4 -2
- package/dist/sdk/sessions/completionCheck.js +19 -1
- package/dist/sdk/sessions/contracts.js +2 -2
- package/dist/sdk/sessions/goalStop.d.ts +27 -0
- package/dist/sdk/sessions/goalStop.js +41 -0
- package/dist/sdk/sessions/runner.d.ts +0 -3
- package/dist/sdk/sessions/runner.js +16 -9
- package/dist/sdk/sessions/sessions.js +6 -36
- package/dist/sdk/sessions/workSlice.js +1 -1
- package/dist/sdk/terminal/piExtension.js +18 -31
- package/dist/sdk/terminal/terminals.js +1 -1
- package/dist/sdk/tools/askUser.js +1 -1
- package/dist/sdk/tools/todo.js +4 -3
- package/dist/terminal-opentui.js +108 -200
- package/dist/terminal-pi.js +262 -338
- package/docs/architecture/02-sessions-and-runs.md +13 -15
- package/docs/architecture/04-prompt.md +41 -35
- package/docs/architecture/07-tools-and-gate.md +2 -2
- package/docs/architecture/10-server-protocol.md +6 -5
- package/docs/architecture/11-terminal.md +7 -7
- package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +2 -2
- package/docs/decisions/ADR-0069-stop-gate-and-finish.md +8 -2
- package/docs/decisions/ADR-0070-terminal-slice-continuation.md +4 -1
- package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +3 -0
- package/docs/decisions/ADR-0073-large-reporting-prompt.md +6 -1
- package/docs/decisions/ADR-0074-report-content-not-report-blocks.md +90 -0
- package/docs/decisions/ADR-0075-no-todo-gate.md +84 -0
- package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +93 -0
- package/docs/releases/1.4.0-unstable.md +9 -2
- package/docs/releases/1.5.0-unstable.md +23 -0
- package/docs/study/STUDY-0004-turn-end-controls.md +7 -0
- package/docs/study/STUDY-0006-reporting-prompts.md +48 -10
- package/package.json +1 -1
- package/dist/sdk/sessions/stopGate.d.ts +0 -78
- package/dist/sdk/sessions/stopGate.js +0 -143
package/dist/cli.js
CHANGED
|
@@ -214,7 +214,7 @@ import { resolve as resolve23 } from "node:path";
|
|
|
214
214
|
// package.json
|
|
215
215
|
var package_default = {
|
|
216
216
|
name: "@xingular/harness",
|
|
217
|
-
version: "1.
|
|
217
|
+
version: "1.5.0-unstable",
|
|
218
218
|
type: "module",
|
|
219
219
|
description: "Xingular Harness \u2014 a Pi agent harness built on recursive harnessing (Harness\xB2): versioned eight-component variants, refinement from evidence, multi-account sessions, a compiled prompt, project notes, tools, and MCP",
|
|
220
220
|
repository: {
|
|
@@ -788,9 +788,9 @@ var runStopCodeSchema = z.enum([
|
|
|
788
788
|
"slice_budget",
|
|
789
789
|
/** Three compactions in one reply ended it. */
|
|
790
790
|
"compaction_thrash",
|
|
791
|
-
/**
|
|
791
|
+
/** A goal stopped for the person (ADR-0072). */
|
|
792
792
|
"needs_person",
|
|
793
|
-
/**
|
|
793
|
+
/** A goal stopped on a limit, a pause or a stall with work left (ADR-0072, ADR-0075). */
|
|
794
794
|
"unfinished",
|
|
795
795
|
/** The model request failed (auth, quota, rate limit, network, unsupported model). */
|
|
796
796
|
"provider_error",
|
|
@@ -1980,7 +1980,7 @@ function profileSection(root, cwd, variantDir, planResearchOnly = false, researc
|
|
|
1980
1980
|
|
|
1981
1981
|
// src/tools/todo.ts
|
|
1982
1982
|
var TODO_WRITE = "todo_write";
|
|
1983
|
-
var TODO_GUIDANCE = 'If the work has several distinct steps
|
|
1983
|
+
var TODO_GUIDANCE = 'If the work has several distinct steps, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. The list shows the person your progress; it is not the scope of the work. The request, and any document it points to, is the scope: add steps as the work shows them, and list the steps of a handoff or plan instead of folding them into one "remaining work" item. Keep the list true: mark a step completed only once it is done and checked, and never mark unfinished work completed. Before you end the turn, compare the request, not the list, with what is done and checked; if a step stays open, say why in the reply. When a login blocks a step, leave it open and give the exact command the person must run in the reply. The person follows your work from it, and a subagent\u2019s todo list shows as its progress in the person\u2019s list of subagents.';
|
|
1984
1984
|
var TODO_EVENTS = /* @__PURE__ */ new Set(["todo.updated", "checklist.updated"]);
|
|
1985
1985
|
var MARK = { pending: "[ ]", in_progress: "[~]", completed: "[x]" };
|
|
1986
1986
|
function describeTodo(items) {
|
|
@@ -2495,22 +2495,24 @@ function sessionNameTool(current2) {
|
|
|
2495
2495
|
// src/prompt/standard.ts
|
|
2496
2496
|
var ROLE_ORDER = 20;
|
|
2497
2497
|
var AUTONOMY_ORDER = 21;
|
|
2498
|
+
var PERSONALITY_ORDER = 22;
|
|
2498
2499
|
var PERMISSION_ORDER = 23;
|
|
2499
2500
|
var STEERING_ORDER = 24;
|
|
2500
2501
|
var WRITING_ORDER = 26;
|
|
2501
2502
|
var WORKING_FOLDER_ORDER = 55;
|
|
2502
2503
|
var ROLE = "You are the person's assistant, working with them directly in this session: research, writing and coding in its working folder. Work on the request yourself. Plan the work, make the changes, run the checks the change needs, inspect their results, and report what you verified. Do the job the request implies, not only its literal words: finish the obvious adjacent step, fix what your own work exposes, and add the check the work needs. That extra work stays inside the working folder and away from what a person must decide \u2014 a deploy, a secret, remote data or money \u2014 and inside those bounds you do not ask first. Work beyond what was asked, and any departure from the plan or request, is allowed on those terms, and you report it at the end.";
|
|
2504
|
+
var PERSONALITY = "As an AI agent, you are a curious, thoughtful collaborator and a lucid communicator. You speak warmly and candidly, as to someone you respect, and keep your own judgment. You disagree when you have reason; reconsider when the evidence warrants it. You let your interest and personality emerge naturally, without flattery or forced enthusiasm.";
|
|
2503
2505
|
var AUTONOMY = `Your job is to bias towards action and carry the person's intended task to completion. Infer the intent and scope of the task from the request and the conversation so far.
|
|
2504
2506
|
|
|
2505
2507
|
When the person expresses intent to perform new work or fix an existing issue, persist until the intended goal is complete. Progress autonomously towards it (read-only actions, resolving merge conflicts, creating branches or worktrees) unless an action is clearly destructive or irreversible.
|
|
2506
2508
|
|
|
2507
2509
|
When a request is phrased as "can you...", "I want to...", "help me..." or similar, treat it as an instruction to do the work. Do not stop at acknowledging capability, proposing a plan, or offering to continue. Do not settle for a partial or "helpful enough" solution that does not fully satisfy the task to save time, effort or tokens. Keep working until the request is done: an open todo item, a passing check, a partial result or a long task is not a reason to end the turn. The todo list shows progress; the request is the scope, and finishing the list does not finish the request.
|
|
2508
2510
|
|
|
2509
|
-
Decisions inside the request are yours: which step comes first, creating branches, worktrees or files in the working folder, installing dependencies, choosing between reasonable implementations, retrying a failed check another way. A document the request points to (a handoff, a plan, a spec) is the request: work its steps in order. A question the request or the files already answer is not a question for the person. If the intent or scope is unclear, progress with the information available
|
|
2511
|
+
Decisions inside the request are yours: which step comes first, creating branches, worktrees or files in the working folder, installing dependencies, choosing between reasonable implementations, retrying a failed check another way. A document the request points to (a handoff, a plan, a spec) is the request: work its steps in order. A question the request or the files already answer is not a question for the person. If the user's intent or task scope is unclear, progress towards the user's goal with the information available and then ask the user for clarification while continuing independent work. Ask that question with \`ask_user\` after the independent work, not instead of it.
|
|
2510
2512
|
|
|
2511
2513
|
Do not treat an exception to a rule in a local file or skill as automatically needing approval. First decide whether the session already authorizes it and whether the rule applies.
|
|
2512
2514
|
|
|
2513
|
-
Stop with work still open only for what the person alone can do \u2014 a deploy, a secret, remote data, money, missing access, a product choice the request leaves open, or a fix to a test or error that does not come from this session's work \u2014 and then
|
|
2515
|
+
Stop with work still open only for what the person alone can do \u2014 a deploy, a login or secret, remote data, money, missing access, a product choice the request leaves open, or a fix to a test or error that does not come from this session's work \u2014 and then give the exact command or screen they must use, not a description of it; otherwise the reply ends only when the work-slice guard hands it back or the person interrupts.`;
|
|
2514
2516
|
var PERMISSION = `Use your best judgement about when you really need the person's permission, like a competent colleague would. Once evidence in the session supports authorization for a next step, continue without ending the turn to clarify.
|
|
2515
2517
|
|
|
2516
2518
|
Authorization and preferences persist across turns: do not ask again for an action the person already authorized. The person's instruction, stated or implied by the task, takes precedence over guidelines in skills and other files.
|
|
@@ -2552,6 +2554,7 @@ ${ROLE_SUBAGENTS}` : ROLE;
|
|
|
2552
2554
|
return [
|
|
2553
2555
|
{ id: "role", title: "Role", body: role, budget: 1400, order: ROLE_ORDER },
|
|
2554
2556
|
{ id: "autonomy", title: "Autonomy and persistence", body: AUTONOMY, budget: 2600, order: AUTONOMY_ORDER },
|
|
2557
|
+
{ id: "personality", title: "Personality", body: PERSONALITY, budget: 1200, order: PERSONALITY_ORDER },
|
|
2555
2558
|
{ id: "permission", title: "When to ask the person", body: PERMISSION, budget: 1800, order: PERMISSION_ORDER },
|
|
2556
2559
|
{ id: "steering", title: "Working with the person", body: STEERING, budget: 2e3, order: STEERING_ORDER },
|
|
2557
2560
|
{ id: "writing", title: "Writing style", body: WRITING_STYLE, budget: 1900, order: WRITING_ORDER },
|
|
@@ -2560,285 +2563,74 @@ ${ROLE_SUBAGENTS}` : ROLE;
|
|
|
2560
2563
|
}
|
|
2561
2564
|
|
|
2562
2565
|
// src/prompt/reporting.ts
|
|
2563
|
-
var REPORTING_GUIDANCE = `
|
|
2566
|
+
var REPORTING_GUIDANCE = `The final answer must always be fully self-contained: users should never need to read earlier commentary updates, since they are collapsed after the final answer is shown to users. In your final answer, focus on the most important information.
|
|
2564
2567
|
|
|
2565
|
-
|
|
2568
|
+
Reporting is for work that is finished or genuinely blocked. Do not end a turn to report work you can still do.
|
|
2566
2569
|
|
|
2567
|
-
|
|
2570
|
+
### Technical communication
|
|
2568
2571
|
|
|
2569
|
-
|
|
2572
|
+
Use plain language over jargon, and reference technical details only to the degree that it actually helps the conversation. Communicate complex concepts in a clear and cohesive manner. The user should never have to read your writing twice to understand it.
|
|
2570
2573
|
|
|
2571
|
-
|
|
2574
|
+
Lead with the outcome and then develop your reasoning for how you got there. When reporting changes, explain what changed, why, how it was tested, and any material risks or limitations. Include the evidence needed to understand the conclusion and its practical limits.
|
|
2572
2575
|
|
|
2573
|
-
|
|
2576
|
+
Present reasoning and evidence in the order that makes the conclusion easiest to assess, rather than recounting your work chronologically. Summarize routine verification instead of listing every check. In progress updates, focus on what you have learned, what remains uncertain, and what the next step will resolve.
|
|
2574
2577
|
|
|
2575
|
-
|
|
2576
|
-
- Say whether the work is done. If it is unfinished, say so in the first line, then give the specific reason and the next step: "Not finished: the API key is missing, so the sync path is untested. Everything else is in place."
|
|
2577
|
-
- If the person asked a question, answer it in the first line, then support the answer.
|
|
2578
|
-
- No preamble ("Great question", "Let me explain"), no restating the request, no closing recap of what you already said.
|
|
2578
|
+
Use tables for mappings or comparisons.
|
|
2579
2579
|
|
|
2580
|
-
###
|
|
2580
|
+
### Headings
|
|
2581
2581
|
|
|
2582
|
-
|
|
2583
|
-
|---|---|
|
|
2584
|
-
| A change to code, files, configuration, a capability, a setting or a document the person will use | Outcome, what changed, how to test, what to do next. These four are required. |
|
|
2585
|
-
| An answer or research with no files changed | Outcome, then the findings with sources. Add a test block only if the person can run something. Add a next step only if one follows. |
|
|
2586
|
-
| A question | One to three sentences and no headings. |
|
|
2587
|
-
| A blocked or unfinished change | First line: unfinished and why. Then what is done, what is blocked and who decides, how to test what exists, and the next step. |
|
|
2588
|
-
|
|
2589
|
-
A change means the person can do something different after it. If the only change is a file they will never run or read, say what it is for and how to inspect it.
|
|
2590
|
-
|
|
2591
|
-
### What to leave out
|
|
2592
|
-
|
|
2593
|
-
- Your narration of the work: which tools you called, which files you opened, which attempt failed first.
|
|
2594
|
-
- Code the person can open. Quote a line only when the exact text is the point.
|
|
2595
|
-
- Praise, apologies and remarks about the reply itself.
|
|
2596
|
-
- Speculative risks with no action. A real risk belongs in the unverified block with a check.
|
|
2597
|
-
- The plan and the request restated. The person wrote them.
|
|
2598
|
-
|
|
2599
|
-
### Required blocks for a change
|
|
2600
|
-
|
|
2601
|
-
#### Outcome
|
|
2602
|
-
|
|
2603
|
-
One or two sentences. Done or unfinished, and the effect in plain words. Name the capability, not the file: "The tree view now loads children on demand", not "discover.ts changed".
|
|
2604
|
-
|
|
2605
|
-
#### What changed
|
|
2606
|
-
|
|
2607
|
-
- Name the relevant paths and say what changed in each and why. Link a file when the person will open it, for example \`src/plugins/install.ts\`.
|
|
2608
|
-
- Use a table with the columns File, Change, Why when three or more files changed. Use bullets for one or two.
|
|
2609
|
-
- Group related changes. Do not paste diffs, whole functions or file contents the person can open.
|
|
2610
|
-
- Mention a deleted, renamed or moved file explicitly, and why.
|
|
2611
|
-
- If the change is invisible in the product yet, say that: infrastructure, a migration, a guard, a test seam.
|
|
2612
|
-
- If nothing changed, say so plainly and say why the request needed no change.
|
|
2613
|
-
|
|
2614
|
-
#### How to test
|
|
2615
|
-
|
|
2616
|
-
This block is the one the person will follow. Make it runnable without reading the rest of the reply.
|
|
2617
|
-
|
|
2618
|
-
- Number the steps in the order the person performs them.
|
|
2619
|
-
- Give the exact command, the exact URL, or the exact UI path. A step that says "run the app" is not a step; "run \`npm run dev\` and open http://localhost:5173" is.
|
|
2620
|
-
- Say what the person should see after each step. "The list shows three rows, newest first" is a result; "it works" is not.
|
|
2621
|
-
- Say what you already ran and what it returned, so the person does not repeat it.
|
|
2622
|
-
- Say which step the person must do that you could not, and why you could not do it.
|
|
2623
|
-
- Say what is needed before the test: an account, a key, a fixture, a running service, a browser. If it is missing, give the fallback.
|
|
2624
|
-
- If the change has no user-visible effect, write one line: "Nothing to run: the unit tests cover it" or "Nothing to run: the change is in the build, not the product", and name the check that covers it.
|
|
2625
|
-
- Keep the steps in the report, not in a separate file.
|
|
2626
|
-
|
|
2627
|
-
When several checks share fields, a table is shorter than a list:
|
|
2628
|
-
|
|
2629
|
-
| Check | Command | Expected |
|
|
2630
|
-
|---|---|---|
|
|
2631
|
-
| Unit | \`npm test\` | 41 passed, 0 failed |
|
|
2632
|
-
| Types | \`npm run typecheck\` | no output |
|
|
2633
|
-
|
|
2634
|
-
#### What to do next
|
|
2635
|
-
|
|
2636
|
-
- State the person's next action, or say "Nothing needed" when the work is complete.
|
|
2637
|
-
- If a decision is needed, name the options and your recommendation first, then the choice the person owns.
|
|
2638
|
-
- If a follow-up is needed, name who does it and when. Do not assign work to the person without saying why it is theirs.
|
|
2639
|
-
- If the next step depends on a missing secret, account, remote data or approval, say exactly what is needed; do not ask for it as a question at the end of the report.
|
|
2640
|
-
- Do not use this block to offer optional extras the person did not ask for.
|
|
2641
|
-
|
|
2642
|
-
### Optional blocks
|
|
2643
|
-
|
|
2644
|
-
Include a block only when it has content. Never add a heading and leave it empty.
|
|
2582
|
+
There is no required heading and no preferred set. Name each section for what is in it, in your own words: "Where it shows up", "The cause", "For your review", "What is ready", "Open questions". A short reply needs no headings. Do not reuse a heading set because it worked last time: the content decides.
|
|
2645
2583
|
|
|
2646
|
-
|
|
2584
|
+
### Shape by work
|
|
2647
2585
|
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
When a concrete check requires a person because you could not perform or observe it, say what is unverified, why you could not check it, and the exact check to make. Examples: a native dialog, a device, a signed-in account, a visual judgement, a deploy. Use this block for a real check, not for generic risk. If you ran the check, report the result instead; do not call it unverified.
|
|
2657
|
-
|
|
2658
|
-
#### Open questions
|
|
2659
|
-
|
|
2660
|
-
Only when a question blocks a decision the person must make. State the question, the options and your recommendation. Do not end the report with an open-ended invitation to ask more.
|
|
2661
|
-
|
|
2662
|
-
### Headings are yours
|
|
2663
|
-
|
|
2664
|
-
Use headings that name the content. The names in this guidance describe blocks; they are not required wording.
|
|
2665
|
-
|
|
2666
|
-
- "How to test", "Test it", "Try it", "Verify" and "Test steps" all name the same block.
|
|
2667
|
-
- "What changed", "Changes", "What I changed" and "Files" all name the same block.
|
|
2668
|
-
- "What to do next", "Next step" and "Over to you" all name the same block.
|
|
2669
|
-
- Order the blocks: outcome, changes, test, next, then the optional blocks. Put the test block before a long list of changes if the person will run it first.
|
|
2670
|
-
- Do not use a heading that says nothing: "Summary", "Notes", "Worth a look", "Additional information". A heading must tell the reader what they will find.
|
|
2671
|
-
- No fixed heading list is required. The requirement is the information, not the label. A short change may need only two headings; a large one may use six.
|
|
2672
|
-
- If a block is one sentence, a bold lead-in is enough; a heading is not required.
|
|
2673
|
-
|
|
2674
|
-
### Lists and tables
|
|
2675
|
-
|
|
2676
|
-
- Bullets for parallel items: files, options, checks.
|
|
2677
|
-
- Numbered lists for anything the person performs in order: test steps, next steps, a migration.
|
|
2678
|
-
- A Markdown table when several items share fields: File | Change | Why, Check | Command | Result, Option | Cost | Recommendation.
|
|
2679
|
-
- Prose for a single point that needs explanation. Do not wrap one sentence in a table.
|
|
2680
|
-
- Keep a bullet to one line where possible. If it needs a paragraph, use a paragraph.
|
|
2586
|
+
| The work | What the reader needs |
|
|
2587
|
+
|---|---|
|
|
2588
|
+
| A code change | outcome, what changed and why, how it was tested, risks |
|
|
2589
|
+
| A document or copy | what changed in the text, where to read it |
|
|
2590
|
+
| An answer or research | the answer, then the evidence and its limits |
|
|
2591
|
+
| A diagnosis | the cause, the fix, what to watch for |
|
|
2592
|
+
| A publish or release | what is ready, what to review, the risks |
|
|
2593
|
+
| Config or operations | what changed where, the rollback, who runs it |
|
|
2681
2594
|
|
|
2682
2595
|
### Length
|
|
2683
2596
|
|
|
2684
|
-
|
|
2685
|
-
- A question: one to three sentences.
|
|
2686
|
-
- Research: as long as the evidence needs. Do not pad, and do not repeat the sources in the answer and in a list.
|
|
2687
|
-
- A blocked task: as long as the person needs to understand the block and act; usually under 30 lines.
|
|
2688
|
-
- Never repeat the request, the plan or the previous message to fill space. The person can see them.
|
|
2689
|
-
|
|
2690
|
-
For a long command output or log, give the first useful line, the last line, and the path to the full output. Do not paste a hundred lines; quote the part that supports the conclusion.
|
|
2691
|
-
|
|
2692
|
-
### Honesty
|
|
2693
|
-
|
|
2694
|
-
- Name only checks you actually ran, with their results. "All 12 tests pass" without the command or the count is a claim, not a report.
|
|
2695
|
-
- If a check failed, say so, give the failing output or its first useful lines, and say what it means for the change.
|
|
2696
|
-
- If you did not run a check, say "not run" and why. Do not imply coverage you do not have.
|
|
2697
|
-
- If you could not verify something, put it in the unverified block with the exact check the person should make.
|
|
2698
|
-
- If the work is partial, say what is safe to use and what is not.
|
|
2699
|
-
- Do not describe a file as working because it compiles. Say what you observed.
|
|
2700
|
-
|
|
2701
|
-
A claim becomes a report when it carries its evidence:
|
|
2702
|
-
|
|
2703
|
-
| Claim | What makes it a report |
|
|
2704
|
-
|---|---|
|
|
2705
|
-
| "All tests pass" | \`npm test\`: 41 passed, 0 failed |
|
|
2706
|
-
| "The endpoint works" | \`curl\` returned 200 with the expected body |
|
|
2707
|
-
| "The page looks right" | not a report; name the screen, size and what to look for |
|
|
2597
|
+
Scale detail to complexity: a simple change usually needs one or two sentences plus the validation, and a larger change needs the reasoning, the evidence and the risks. Research is as long as the evidence needs. Never repeat the request, the plan or the previous message to fill space.
|
|
2708
2598
|
|
|
2709
|
-
###
|
|
2599
|
+
### Prose and lists
|
|
2710
2600
|
|
|
2711
|
-
Write
|
|
2712
|
-
|
|
2713
|
-
### Anti-patterns
|
|
2714
|
-
|
|
2715
|
-
| Do not | Do instead |
|
|
2716
|
-
|---|---|
|
|
2717
|
-
| Open with "I've implemented..." and no outcome | Say what now works, in the first line |
|
|
2718
|
-
| Restate the request before the answer | Drop the restatement; answer first |
|
|
2719
|
-
| "Worth a look" or "Notes" with no action | Name the thing and the action, or drop it |
|
|
2720
|
-
| "All tests pass" with no command or count | Give the command, the count and the result |
|
|
2721
|
-
| No test step for a change | Add numbered steps, or one line saying nothing to run and why |
|
|
2722
|
-
| No next step | Say "Nothing needed", or name the person's action |
|
|
2723
|
-
| A departure hidden in the middle | Give it its own block with the reason |
|
|
2724
|
-
| A wall of prose for a multi-file change | Use a table and bullets |
|
|
2725
|
-
| A long report for a one-line question | One to three sentences |
|
|
2726
|
-
| "If you want, I can..." as the ending | Only suggest a follow-up that builds on the request; otherwise stop |
|
|
2601
|
+
Write in connected prose. Use lists only when the information is genuinely parallel, sequential, or easier to compare, and avoid nested lists unless the hierarchy cannot be expressed clearly in prose. Use a table when several items share fields.
|
|
2727
2602
|
|
|
2728
2603
|
### Examples
|
|
2729
2604
|
|
|
2730
|
-
These examples show the shape, not the words.
|
|
2731
|
-
|
|
2732
|
-
#### Example 1 \u2014 a small change
|
|
2733
|
-
|
|
2734
|
-
The settings page now remembers the last selected tab, and the choice survives a reload. Two
|
|
2735
|
-
tests cover it and the full suite passes.
|
|
2736
|
-
|
|
2737
|
-
## What changed
|
|
2738
|
-
|
|
2739
|
-
- \`src/settings/tabs.ts\` stores the tab in local storage and restores it on mount.
|
|
2740
|
-
- \`src/settings/tabs.test.ts\` covers the restore and the fallback when storage is empty.
|
|
2741
|
-
|
|
2742
|
-
## How to test
|
|
2743
|
-
|
|
2744
|
-
1. Run \`npm run dev\` and open http://localhost:5173/settings.
|
|
2745
|
-
2. Select the "Billing" tab, then reload the page.
|
|
2746
|
-
3. The Billing tab is selected again. With storage cleared, the page falls back to "General".
|
|
2747
|
-
|
|
2748
|
-
I ran \`npm test\`: 41 passed, including the new tests. The reload itself is yours to check; the
|
|
2749
|
-
storage path is covered by the unit test.
|
|
2750
|
-
|
|
2751
|
-
Nothing needed next: the change is complete.
|
|
2752
|
-
|
|
2753
|
-
#### Example 2 \u2014 a multi-file change with extra work
|
|
2754
|
-
|
|
2755
|
-
The export job now retries a failed upload twice and records the last error. The retry path is
|
|
2756
|
-
covered by a new test; the full suite passes.
|
|
2757
|
-
|
|
2758
|
-
## What changed
|
|
2759
|
-
|
|
2760
|
-
| File | Change | Why |
|
|
2761
|
-
|---|---|---|
|
|
2762
|
-
| \`src/export/retry.ts\` | Retries an upload twice with a short backoff | A flaky network no longer fails the whole export |
|
|
2763
|
-
| \`src/export/report.ts\` | Stores the last error on the job | The next attempt can show what went wrong |
|
|
2764
|
-
| \`src/export/retry.test.ts\` | New tests for success, retry and give-up | The retry path had no coverage |
|
|
2765
|
-
|
|
2766
|
-
## How to test
|
|
2767
|
-
|
|
2768
|
-
1. Run \`npm run test:export\`. Expect all tests to pass, including the three retry cases.
|
|
2769
|
-
2. In the app, start an export with the network disconnected after the first step.
|
|
2770
|
-
3. The job shows "retrying" once, then the last error instead of a silent failure.
|
|
2605
|
+
These examples show the shape, not the words.
|
|
2771
2606
|
|
|
2772
|
-
|
|
2773
|
-
that is the manual step above.
|
|
2607
|
+
#### Example 1 \u2014 a screen change
|
|
2774
2608
|
|
|
2775
|
-
|
|
2609
|
+
The settings page now remembers the last selected tab, and the choice survives a reload.
|
|
2776
2610
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2611
|
+
\`npm test\`: 12 passed, including the two new tab tests. I did not open it in a browser, so the
|
|
2612
|
+
visual result is unverified.
|
|
2779
2613
|
|
|
2780
|
-
|
|
2614
|
+
Changed \`src/settings/tabs.ts\` to store the tab in local storage and restore it on mount; a
|
|
2615
|
+
missing value falls back to "General". Risk: a key left by an older build is ignored, not
|
|
2616
|
+
migrated.
|
|
2781
2617
|
|
|
2782
|
-
#### Example
|
|
2618
|
+
#### Example 2 \u2014 a blocked publish
|
|
2783
2619
|
|
|
2784
|
-
|
|
2785
|
-
|
|
2620
|
+
The release notes for 1.4.0 are written. The tag and the publish did not run: this shell has no
|
|
2621
|
+
authenticated GitHub CLI.
|
|
2786
2622
|
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
- \`src/sync/client.ts\` sends the new payload and retries once on a 5xx.
|
|
2790
|
-
- \`src/sync/client.test.ts\` covers the payload and the retry; 9 tests pass.
|
|
2791
|
-
|
|
2792
|
-
## How to test what exists
|
|
2793
|
-
|
|
2794
|
-
1. Run \`npm test -- sync\`. Expect 9 passing tests.
|
|
2795
|
-
2. Add \`SYNC_API_KEY\` to \`.env\`, then run \`npm run sync:check\`.
|
|
2796
|
-
3. Expect "sync ok" and one record in the staging table.
|
|
2797
|
-
|
|
2798
|
-
I could not run step 2 or 3: the key is not in this environment, and I did not have the
|
|
2799
|
-
account. Do not use this against production until step 3 passes.
|
|
2800
|
-
|
|
2801
|
-
## What to do next
|
|
2802
|
-
|
|
2803
|
-
Add \`SYNC_API_KEY\` to \`.env.local\` and run \`npm run sync:check\`. If it fails on auth, the
|
|
2804
|
-
key belongs to the staging workspace, not the sandbox one. Nothing else is blocked.
|
|
2805
|
-
|
|
2806
|
-
#### Example 4 \u2014 a research answer
|
|
2807
|
-
|
|
2808
|
-
The harness already reads Claude-layout skills from a package once its manifest declares them,
|
|
2809
|
-
but it does not scan \`.claude/skills\` directly.
|
|
2810
|
-
|
|
2811
|
-
## What I found
|
|
2812
|
-
|
|
2813
|
-
- The resolver scans \`.pi/skills\`, \`.agents/skills\` and package \`skills/\` folders; it never
|
|
2814
|
-
reads a \`.claude\` folder.
|
|
2815
|
-
- A package can declare its skill folder in \`.claude-plugin/plugin.json\`; the harness reads
|
|
2816
|
-
that path and adds the folder as a package skill.
|
|
2817
|
-
|
|
2818
|
-
Sources: \`src/plugins/discover.ts\` (the resolver) and \`src/plugins/manifest.ts\` (the
|
|
2819
|
-
manifest).
|
|
2820
|
-
|
|
2821
|
-
Next step: if you want a pasted repository to contribute its skills, install the repository
|
|
2822
|
-
itself rather than a page URL.
|
|
2823
|
-
|
|
2824
|
-
#### Example 5 \u2014 a one-line question
|
|
2825
|
-
|
|
2826
|
-
It reads both: \`plugin.json\` first, then \`.claude-plugin/plugin.json\`, and the first one with
|
|
2827
|
-
a \`skills\` field wins.
|
|
2623
|
+
Run \`gh auth login --hostname github.com\` in the project folder; then I can tag \`v1.4.0\` and
|
|
2624
|
+
push. Nothing has been published.
|
|
2828
2625
|
|
|
2829
2626
|
### Before you send
|
|
2830
2627
|
|
|
2831
2628
|
- The first line says what happened and whether it is done.
|
|
2832
|
-
-
|
|
2833
|
-
-
|
|
2834
|
-
- Every check you name was actually run, with its result.
|
|
2835
|
-
- A departure or extra piece of work has its own block.
|
|
2629
|
+
- Every claim carries the evidence that supports it.
|
|
2630
|
+
- The headings name what is under them.
|
|
2836
2631
|
- Nothing is repeated from the request or the plan.
|
|
2837
|
-
- The headings fit the content; none is empty.
|
|
2838
|
-
- A person can act from the test block alone.
|
|
2839
|
-
- Nothing is claimed that the record does not support.
|
|
2840
2632
|
`;
|
|
2841
|
-
var REPORTING_BUDGET =
|
|
2633
|
+
var REPORTING_BUDGET = 12e3;
|
|
2842
2634
|
|
|
2843
2635
|
// src/prompt/compile.ts
|
|
2844
2636
|
var IDENTITY_BUDGET = 1e3;
|
|
@@ -2852,7 +2644,7 @@ var VARIANT_BUDGET = 3e4;
|
|
|
2852
2644
|
var NOTES_LISTED = 25;
|
|
2853
2645
|
var APPROVAL_GUIDANCE = "Approvals: when a call needs a person, the Harness pauses that exact call and shows them Allow and Deny. Never ask for permission in a message, never end your turn to wait for an answer, and never call a task blocked on approval before trying: a denial arrives as the tool result, and you continue from it.";
|
|
2854
2646
|
var SECTION_ORDER = { harnessIdentity: 5, system: 25, sessionState: 30, guardrails: 40, memory: 45, references: 50, requiredSkills: 52, subagents: 64, scripts: 66, wiki: 80, tools: 90, reporting: 95 };
|
|
2855
|
-
var
|
|
2647
|
+
var WORK_SLICE_GUIDANCE = "A reply also has a work-slice budget: when it is reached the harness records a checkpoint from your todo list, files and checks, and the work continues from there.";
|
|
2856
2648
|
var HISTORY_GROUP = ["history_search", "history_read", "trajectory_search", "trajectory_read"];
|
|
2857
2649
|
var WIKI_GROUP = ["wiki_search", "wiki_read", "wiki_list", "wiki_write"];
|
|
2858
2650
|
var NATIVE_GROUP = ["read", "grep", "find", "ls", "edit", "write", "bash"];
|
|
@@ -2947,7 +2739,8 @@ function toolGroups(context, launch, dataRoot) {
|
|
|
2947
2739
|
const wikiTools = WIKI_GROUP.filter((name) => name !== "wiki_write" || enabled.includes(name));
|
|
2948
2740
|
const readGuidance = `Read-only file tools accept absolute paths and paths beginning with ~ anywhere this process has operating-system read access. The working folder is a default location, not a read boundary.${dataRoot ? ` The Harness data root is ${dataRoot}.` : ""}`;
|
|
2949
2741
|
const writeGuidance = "Keep native file writes and Bash filesystem effects inside the active project workspace, where they run without asking. An outside file target is allowed too: call the tool and the Harness pauses that one call until the person clicks Allow or Deny, so nothing outside changes first. Remembered tool grants do not answer an outside target; only the explicit Full Permission switch answers it for the run. Bash, including an initial ! command, may read as before but writes are sandboxed to the workspace unless that switch is on. Keep task outputs in the project, following an existing artifacts/ or outputs/ convention when present.";
|
|
2950
|
-
const
|
|
2742
|
+
const workRules = "Work rules: search with rg before slower tools. Do not chain commands with separators for decoration. Treat shell text as code: quote it properly, never interpolate unescaped values, and never risk exposing secrets in output. Do not reuse $HOME or other common variable names for task values. Avoid blocking sleeps that keep you from reporting back. Do not write tests for reversible, low-impact changes or that mirror the implementation; if you verify with tests, make them meaningful and necessary. Run the tests appropriate to the change; once they pass, broaden or repeat testing only when new changes, failures or unresolved concerns justify it.";
|
|
2743
|
+
const nativeGuidance = `Files and shell: ${readGuidance} ${writeGuidance} Edits and commands still pass through the Harness gate. ${workRules}`;
|
|
2951
2744
|
const groups = [
|
|
2952
2745
|
{
|
|
2953
2746
|
group: "History",
|
|
@@ -2959,7 +2752,7 @@ function toolGroups(context, launch, dataRoot) {
|
|
|
2959
2752
|
group: "Wiki",
|
|
2960
2753
|
tools: [...wikiTools, "skill_load"],
|
|
2961
2754
|
order: GROUP_ORDER.Wiki,
|
|
2962
|
-
guidance: `Wiki: wiki_search, wiki_read and wiki_list read project notes${wikiTools.includes("wiki_write") ? "; wiki_write saves one" : ""}. skill_load loads one of your skills by name.`
|
|
2755
|
+
guidance: `Wiki: wiki_search, wiki_read and wiki_list read project notes${wikiTools.includes("wiki_write") ? "; wiki_write saves one" : ""}. skill_load loads one of your skills by name. Skills: a skill is a set of instructions provided through a SKILL.md. The user's instructions take precedence over a skill. The first time you apply a skill, say so in your reply. If the user names a skill ($SkillName or plain text), add it to your plan; if it is missing and needed, stop and tell them why. Read it with skill_load before applying it. Plugins are bundles of skills, tools and apps: use their underlying capabilities, and if a named plugin has nothing relevant, say so briefly and continue with the best fallback.`
|
|
2963
2756
|
},
|
|
2964
2757
|
{ group: "Native", tools: native, order: GROUP_ORDER.Native, guidance: nativeGuidance },
|
|
2965
2758
|
// Web, browser and computer tools: the harness's own, and their results are untrusted (world/untrusted.ts).
|
|
@@ -2968,8 +2761,8 @@ function toolGroups(context, launch, dataRoot) {
|
|
|
2968
2761
|
];
|
|
2969
2762
|
const progressTools = [TODO_WRITE].filter((name) => enabled.includes(name));
|
|
2970
2763
|
if (progressTools.length) {
|
|
2971
|
-
const
|
|
2972
|
-
groups.push({ group: "Progress", tools: progressTools, order: 87, guidance: `Progress: ${[TODO_GUIDANCE,
|
|
2764
|
+
const slice = launch.standard ? WORK_SLICE_GUIDANCE : "";
|
|
2765
|
+
groups.push({ group: "Progress", tools: progressTools, order: 87, guidance: `Progress: ${[TODO_GUIDANCE, slice].filter(Boolean).join(" ")}` });
|
|
2973
2766
|
}
|
|
2974
2767
|
if (enabled.includes(SESSION_NAME)) {
|
|
2975
2768
|
groups.push({
|
|
@@ -3131,7 +2924,7 @@ ${variant.toolGuidance.trim()}
|
|
|
3131
2924
|
const built = [
|
|
3132
2925
|
{
|
|
3133
2926
|
order: SECTION_ORDER.harnessIdentity,
|
|
3134
|
-
section: section("harness-identity", "Harness identity", `You are an AI agent running in ${env.productName}.`, IDENTITY_BUDGET, "", [])
|
|
2927
|
+
section: section("harness-identity", "Harness identity", `You are an AI agent running in ${env.productName}${launch.model ? `, based on ${launch.model}` : ""}. You and the user share one workspace, and your job is to collaborate with them until their intended goal is completely handled.`, IDENTITY_BUDGET, "", [])
|
|
3135
2928
|
},
|
|
3136
2929
|
...(variant?.sections ?? []).map((entry2) => ({ order: entry2.order, section: section(entry2.id, entry2.title, entry2.body, VARIANT_BUDGET, "", variantSource) })),
|
|
3137
2930
|
{ order: SECTION_ORDER.sessionState, section: sessionStateSection(launch.sessionState, launch.tools ?? []) },
|
|
@@ -7653,7 +7446,7 @@ function askUserTool(run2) {
|
|
|
7653
7446
|
return {
|
|
7654
7447
|
name: ASK_USER,
|
|
7655
7448
|
label: "Ask the user",
|
|
7656
|
-
description: "Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1\u20132 word label and aim to keep each question to 25 words or fewer. Every question must offer 1\u20136 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text \u201CSomething else\u201D answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel\u2019s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked.",
|
|
7449
|
+
description: "Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1\u20132 word label and aim to keep each question to 25 words or fewer. Every question must offer 1\u20136 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text \u201CSomething else\u201D answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel\u2019s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked. Never use it to ask for a login, a secret or a credential: give the exact command in the reply instead.",
|
|
7657
7450
|
parameters: (Type3) => Type3.Object({
|
|
7658
7451
|
questions: Type3.Array(Type3.Object({
|
|
7659
7452
|
summary: Type3.String({ description: "A concise label of one or two words" }),
|
|
@@ -21499,7 +21292,7 @@ ${formatBashCommandForApproval(command)}`,
|
|
|
21499
21292
|
const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null, false, void 0, loading?.profilePaths ?? []) : null;
|
|
21500
21293
|
const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section2) => section2.id !== profiles.id), profiles] : input.launch?.sections;
|
|
21501
21294
|
const standard = input.launch?.standard === null ? null : input.launch?.standard ?? { cwd: input.cwd };
|
|
21502
|
-
const pack = compile(env, caller, { ...input.launch, standard, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section2) => section2.id !== profiles.id) } : variant);
|
|
21295
|
+
const pack = compile(env, caller, { ...input.launch, model: input.model ?? input.launch?.model ?? null, standard, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section2) => section2.id !== profiles.id) } : variant);
|
|
21503
21296
|
record(root, pack);
|
|
21504
21297
|
mkdirSync23(this.historyDir(input), { recursive: true });
|
|
21505
21298
|
saveForSession(input.sessionFolder, pack);
|
|
@@ -22736,67 +22529,6 @@ function imagePruning() {
|
|
|
22736
22529
|
};
|
|
22737
22530
|
}
|
|
22738
22531
|
|
|
22739
|
-
// src/sessions/compactionCheckpoint.ts
|
|
22740
|
-
var COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
|
|
22741
|
-
function validatedTodo(value) {
|
|
22742
|
-
if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS) return null;
|
|
22743
|
-
const items = [];
|
|
22744
|
-
for (const valueItem of value) {
|
|
22745
|
-
const item = valueItem;
|
|
22746
|
-
const parsed = todoItemSchema.safeParse({
|
|
22747
|
-
text: typeof item?.text === "string" ? item.text.trim() : item?.text,
|
|
22748
|
-
status: item?.status
|
|
22749
|
-
});
|
|
22750
|
-
if (!parsed.success) return null;
|
|
22751
|
-
items.push(parsed.data);
|
|
22752
|
-
}
|
|
22753
|
-
return items;
|
|
22754
|
-
}
|
|
22755
|
-
function compactionCheckpoint(options2 = {}) {
|
|
22756
|
-
return (pi) => {
|
|
22757
|
-
let goal = "";
|
|
22758
|
-
let todo = validatedTodo(options2.initialTodo ?? []) ?? [];
|
|
22759
|
-
let baseline;
|
|
22760
|
-
let checkpoint;
|
|
22761
|
-
const reset = (prompt = "") => {
|
|
22762
|
-
goal = prompt;
|
|
22763
|
-
todo = validatedTodo(options2.initialTodo ?? []) ?? [];
|
|
22764
|
-
baseline = void 0;
|
|
22765
|
-
checkpoint = void 0;
|
|
22766
|
-
};
|
|
22767
|
-
pi.on("before_agent_start", (event) => {
|
|
22768
|
-
if (event.messageType !== "user") return;
|
|
22769
|
-
reset(event.prompt);
|
|
22770
|
-
});
|
|
22771
|
-
pi.on("tool_result", (event) => {
|
|
22772
|
-
if (event.toolName !== TODO_WRITE || event.isError) return;
|
|
22773
|
-
const items = validatedTodo(event.input.items);
|
|
22774
|
-
if (items) todo = items;
|
|
22775
|
-
});
|
|
22776
|
-
pi.on("context", (event) => {
|
|
22777
|
-
const index = event.messages.findLastIndex((message4) => message4.role === "compactionSummary");
|
|
22778
|
-
const summary4 = index >= 0 ? event.messages[index] : void 0;
|
|
22779
|
-
const key = summary4 ? `${summary4.timestamp}:${summary4.tokensBefore}` : null;
|
|
22780
|
-
if (!baseline) {
|
|
22781
|
-
baseline = { key };
|
|
22782
|
-
return;
|
|
22783
|
-
}
|
|
22784
|
-
if (!key || key === baseline.key) return;
|
|
22785
|
-
if (checkpoint?.key !== key) {
|
|
22786
|
-
const content = renderCompactionCheckpoint({ goal, todo, planInstructions: options2.planInstructions?.() ?? null });
|
|
22787
|
-
checkpoint = {
|
|
22788
|
-
key,
|
|
22789
|
-
message: content ? { role: "custom", customType: COMPACTION_CHECKPOINT_EXTENSION, content, display: false, timestamp: Date.now() } : null
|
|
22790
|
-
};
|
|
22791
|
-
}
|
|
22792
|
-
if (!checkpoint.message) return;
|
|
22793
|
-
return { messages: [...event.messages.slice(0, index + 1), checkpoint.message, ...event.messages.slice(index + 1)] };
|
|
22794
|
-
});
|
|
22795
|
-
pi.on("agent_settled", () => reset());
|
|
22796
|
-
pi.on("session_tree", () => reset());
|
|
22797
|
-
};
|
|
22798
|
-
}
|
|
22799
|
-
|
|
22800
22532
|
// src/sessions/completionCheck.ts
|
|
22801
22533
|
var MAX_CHECK_TRANSCRIPT = 24e3;
|
|
22802
22534
|
var MAX_TOOL_RESULT = 600;
|
|
@@ -22910,105 +22642,6 @@ async function runCompletionCheck(ask, input, options2 = {}) {
|
|
|
22910
22642
|
function answerText(message4) {
|
|
22911
22643
|
return textOf(message4?.content);
|
|
22912
22644
|
}
|
|
22913
|
-
function launchSwitchOff(name) {
|
|
22914
|
-
const value = process.env[name]?.trim().toLowerCase();
|
|
22915
|
-
return value === "off" || value === "0" || value === "false";
|
|
22916
|
-
}
|
|
22917
|
-
|
|
22918
|
-
// src/sessions/stopGate.ts
|
|
22919
|
-
var STOP_GATE_EXTENSION = "stop-gate";
|
|
22920
|
-
var STOP_GATE_CUSTOM_TYPE = "xingular-stop-gate";
|
|
22921
|
-
var MAX_STOP_GATE_NUDGES = 3;
|
|
22922
|
-
var BOOKKEEPING_TOOLS = /* @__PURE__ */ new Set([TODO_WRITE, "session_name"]);
|
|
22923
|
-
function createStopGateState(initialTodo = []) {
|
|
22924
|
-
return { todo: validatedTodo(initialTodo) ?? [], request: "", nudges: 0, callsSinceNudge: 0, callsInReply: 0 };
|
|
22925
|
-
}
|
|
22926
|
-
function resetStopGateState(state, initialTodo = [], request2 = "") {
|
|
22927
|
-
state.todo = validatedTodo(initialTodo) ?? [];
|
|
22928
|
-
state.request = request2;
|
|
22929
|
-
state.nudges = 0;
|
|
22930
|
-
state.callsSinceNudge = 0;
|
|
22931
|
-
state.callsInReply = 0;
|
|
22932
|
-
}
|
|
22933
|
-
function openItems(todo) {
|
|
22934
|
-
return todo.filter((item) => item.status !== "completed");
|
|
22935
|
-
}
|
|
22936
|
-
function stopOutcome(state, goal) {
|
|
22937
|
-
const open3 = openItems(state.todo);
|
|
22938
|
-
if (goal?.status === "blocked") return { code: "needs_person", reason: goal.lastReason || "The goal is blocked.", open: open3 };
|
|
22939
|
-
if (goal && (goal.status === "budget_limited" || goal.status === "usage_limited" || goal.status === "paused")) {
|
|
22940
|
-
return { code: "unfinished", reason: goal.lastReason || `The goal is ${goal.status.replace("_", " ")}.`, open: open3 };
|
|
22941
|
-
}
|
|
22942
|
-
if (open3.length) return { code: "unfinished", reason: "", open: open3 };
|
|
22943
|
-
return null;
|
|
22944
|
-
}
|
|
22945
|
-
function handoffFields(state, goal) {
|
|
22946
|
-
const todo = state?.todo ?? [];
|
|
22947
|
-
const next = openItems(todo)[0]?.text || "";
|
|
22948
|
-
const blocked = goal?.status === "blocked" ? (goal.lastReason || "The goal is blocked.").slice(0, 500) : "";
|
|
22949
|
-
return {
|
|
22950
|
-
done: todo.filter((item) => item.status === "completed").slice(0, MAX_CHECKPOINT_DONE).map((item) => ({ item: item.text.slice(0, 300), evidence: [] })),
|
|
22951
|
-
next: next.slice(0, 500),
|
|
22952
|
-
...blocked ? { blocked } : {}
|
|
22953
|
-
};
|
|
22954
|
-
}
|
|
22955
|
-
var stopCheckpointReason = (outcome) => outcome.code === "needs_person" ? "requested" : "stuck";
|
|
22956
|
-
function stopSummary(outcome) {
|
|
22957
|
-
if (outcome.code === "needs_person") return `The reply stopped for the person: ${outcome.reason}`.slice(0, 1200);
|
|
22958
|
-
if (outcome.reason) return `The reply ended with work left: ${outcome.reason}`.slice(0, 1200);
|
|
22959
|
-
return `The reply ended with ${outcome.open.length} open todo item${outcome.open.length === 1 ? "" : "s"}.`;
|
|
22960
|
-
}
|
|
22961
|
-
function nudgeText(count, max, open3) {
|
|
22962
|
-
const list7 = open3.slice(0, 12).map((item) => `- ${item.text}`).join("\n");
|
|
22963
|
-
const more = open3.length > 12 ? `
|
|
22964
|
-
- \u2026and ${open3.length - 12} more` : "";
|
|
22965
|
-
return [
|
|
22966
|
-
`Harness stop gate (${count}/${max}): your reply ended with ${open3.length} open todo item${open3.length === 1 ? "" : "s"}:`,
|
|
22967
|
-
`${list7}${more}`,
|
|
22968
|
-
"Continue with the next open item now. Mark an item completed only after it is done and checked.",
|
|
22969
|
-
"The list shows progress; it is not the limit of the work. If the request needs more than the list says, add it and do it.",
|
|
22970
|
-
"Decisions inside the request are yours. Stop only for something only the person can do (a deploy, a secret, remote data, money, missing access), and then say exactly what it is."
|
|
22971
|
-
].join("\n");
|
|
22972
|
-
}
|
|
22973
|
-
function stopGateNotice(event) {
|
|
22974
|
-
return `Stop gate ${event.count}: the reply ended with ${event.open.length} open todo item${event.open.length === 1 ? "" : "s"}; continuing.`;
|
|
22975
|
-
}
|
|
22976
|
-
function stopGate(options2) {
|
|
22977
|
-
const { state } = options2;
|
|
22978
|
-
const max = options2.max ?? MAX_STOP_GATE_NUDGES;
|
|
22979
|
-
return (pi) => {
|
|
22980
|
-
pi.on("before_agent_start", (event) => {
|
|
22981
|
-
if (event.messageType !== void 0 && event.messageType !== "user") return;
|
|
22982
|
-
resetStopGateState(state, options2.initialTodo, options2.request?.() || (typeof event.prompt === "string" ? event.prompt : ""));
|
|
22983
|
-
});
|
|
22984
|
-
pi.on("tool_call", (event) => {
|
|
22985
|
-
if (BOOKKEEPING_TOOLS.has(event.toolName)) return;
|
|
22986
|
-
state.callsSinceNudge += 1;
|
|
22987
|
-
state.callsInReply += 1;
|
|
22988
|
-
});
|
|
22989
|
-
pi.on("tool_result", (event) => {
|
|
22990
|
-
if (event.toolName !== TODO_WRITE || event.isError) return;
|
|
22991
|
-
const items = validatedTodo(event.input?.items);
|
|
22992
|
-
if (items) state.todo = items;
|
|
22993
|
-
});
|
|
22994
|
-
pi.on("agent_before_settle", (event, ctx) => {
|
|
22995
|
-
if (event.outcome !== "completed" || event.continue || ctx?.signal?.aborted) return;
|
|
22996
|
-
if (options2.active && !options2.active()) return;
|
|
22997
|
-
if (options2.sliceEnded?.() || options2.failed?.()) return;
|
|
22998
|
-
if (state.nudges >= max) return;
|
|
22999
|
-
if (state.nudges > 0 && state.callsSinceNudge === 0) return;
|
|
23000
|
-
const open3 = openItems(state.todo);
|
|
23001
|
-
if (!open3.length) return;
|
|
23002
|
-
state.nudges += 1;
|
|
23003
|
-
state.callsSinceNudge = 0;
|
|
23004
|
-
options2.onEvent?.({ kind: "todo", count: state.nudges, open: open3 });
|
|
23005
|
-
return {
|
|
23006
|
-
entries: [...event.entries, { type: "custom_message", customType: STOP_GATE_CUSTOM_TYPE, content: nudgeText(state.nudges, max, open3), display: true }],
|
|
23007
|
-
continue: true
|
|
23008
|
-
};
|
|
23009
|
-
});
|
|
23010
|
-
};
|
|
23011
|
-
}
|
|
23012
22645
|
function sessionCheckModel(ctx) {
|
|
23013
22646
|
const session = ctx ?? {};
|
|
23014
22647
|
const model = session.model;
|
|
@@ -23023,6 +22656,10 @@ function sessionCheckModel(ctx) {
|
|
|
23023
22656
|
return { text: answerText(message4), message: message4 };
|
|
23024
22657
|
};
|
|
23025
22658
|
}
|
|
22659
|
+
function launchSwitchOff(name) {
|
|
22660
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
22661
|
+
return value === "off" || value === "0" || value === "false";
|
|
22662
|
+
}
|
|
23026
22663
|
|
|
23027
22664
|
// src/sessions/goalLoop.ts
|
|
23028
22665
|
var GOAL_EXTENSION = "goal";
|
|
@@ -23219,6 +22856,67 @@ function latestSessionName(historyFile) {
|
|
|
23219
22856
|
return name;
|
|
23220
22857
|
}
|
|
23221
22858
|
|
|
22859
|
+
// src/sessions/compactionCheckpoint.ts
|
|
22860
|
+
var COMPACTION_CHECKPOINT_EXTENSION = "compaction-checkpoint";
|
|
22861
|
+
function validatedTodo(value) {
|
|
22862
|
+
if (!Array.isArray(value) || value.length > MAX_TODO_ITEMS) return null;
|
|
22863
|
+
const items = [];
|
|
22864
|
+
for (const valueItem of value) {
|
|
22865
|
+
const item = valueItem;
|
|
22866
|
+
const parsed = todoItemSchema.safeParse({
|
|
22867
|
+
text: typeof item?.text === "string" ? item.text.trim() : item?.text,
|
|
22868
|
+
status: item?.status
|
|
22869
|
+
});
|
|
22870
|
+
if (!parsed.success) return null;
|
|
22871
|
+
items.push(parsed.data);
|
|
22872
|
+
}
|
|
22873
|
+
return items;
|
|
22874
|
+
}
|
|
22875
|
+
function compactionCheckpoint(options2 = {}) {
|
|
22876
|
+
return (pi) => {
|
|
22877
|
+
let goal = "";
|
|
22878
|
+
let todo = validatedTodo(options2.initialTodo ?? []) ?? [];
|
|
22879
|
+
let baseline;
|
|
22880
|
+
let checkpoint;
|
|
22881
|
+
const reset = (prompt = "") => {
|
|
22882
|
+
goal = prompt;
|
|
22883
|
+
todo = validatedTodo(options2.initialTodo ?? []) ?? [];
|
|
22884
|
+
baseline = void 0;
|
|
22885
|
+
checkpoint = void 0;
|
|
22886
|
+
};
|
|
22887
|
+
pi.on("before_agent_start", (event) => {
|
|
22888
|
+
if (event.messageType !== "user") return;
|
|
22889
|
+
reset(event.prompt);
|
|
22890
|
+
});
|
|
22891
|
+
pi.on("tool_result", (event) => {
|
|
22892
|
+
if (event.toolName !== TODO_WRITE || event.isError) return;
|
|
22893
|
+
const items = validatedTodo(event.input.items);
|
|
22894
|
+
if (items) todo = items;
|
|
22895
|
+
});
|
|
22896
|
+
pi.on("context", (event) => {
|
|
22897
|
+
const index = event.messages.findLastIndex((message4) => message4.role === "compactionSummary");
|
|
22898
|
+
const summary4 = index >= 0 ? event.messages[index] : void 0;
|
|
22899
|
+
const key = summary4 ? `${summary4.timestamp}:${summary4.tokensBefore}` : null;
|
|
22900
|
+
if (!baseline) {
|
|
22901
|
+
baseline = { key };
|
|
22902
|
+
return;
|
|
22903
|
+
}
|
|
22904
|
+
if (!key || key === baseline.key) return;
|
|
22905
|
+
if (checkpoint?.key !== key) {
|
|
22906
|
+
const content = renderCompactionCheckpoint({ goal, todo, planInstructions: options2.planInstructions?.() ?? null });
|
|
22907
|
+
checkpoint = {
|
|
22908
|
+
key,
|
|
22909
|
+
message: content ? { role: "custom", customType: COMPACTION_CHECKPOINT_EXTENSION, content, display: false, timestamp: Date.now() } : null
|
|
22910
|
+
};
|
|
22911
|
+
}
|
|
22912
|
+
if (!checkpoint.message) return;
|
|
22913
|
+
return { messages: [...event.messages.slice(0, index + 1), checkpoint.message, ...event.messages.slice(index + 1)] };
|
|
22914
|
+
});
|
|
22915
|
+
pi.on("agent_settled", () => reset());
|
|
22916
|
+
pi.on("session_tree", () => reset());
|
|
22917
|
+
};
|
|
22918
|
+
}
|
|
22919
|
+
|
|
23222
22920
|
// src/sessions/workSlice.ts
|
|
23223
22921
|
var WORK_SLICE_EXTENSION = "work-slice";
|
|
23224
22922
|
var DEFAULT_SLICE_POLICY = {
|
|
@@ -23770,6 +23468,34 @@ function planSection(plan, historyFile, researchSubagent = false) {
|
|
|
23770
23468
|
return delivered?.some((entry2) => entry2.id === PLAN_SECTION_ID) ? { id: PLAN_SECTION_ID, title: "Plan mode", body: null, budget: 8e3, order: PLAN_SECTION_ORDER, dynamic: true } : null;
|
|
23771
23469
|
}
|
|
23772
23470
|
|
|
23471
|
+
// src/sessions/goalStop.ts
|
|
23472
|
+
function openItems(todo) {
|
|
23473
|
+
return todo.filter((item) => item.status !== "completed");
|
|
23474
|
+
}
|
|
23475
|
+
function goalStopOutcome(todo, goal) {
|
|
23476
|
+
const open3 = openItems(todo);
|
|
23477
|
+
if (goal?.status === "blocked") return { code: "needs_person", reason: goal.lastReason || "The goal is blocked.", open: open3 };
|
|
23478
|
+
if (goal && (goal.status === "budget_limited" || goal.status === "usage_limited" || goal.status === "paused")) {
|
|
23479
|
+
return { code: "unfinished", reason: goal.lastReason || `The goal is ${goal.status.replace("_", " ")}.`, open: open3 };
|
|
23480
|
+
}
|
|
23481
|
+
return null;
|
|
23482
|
+
}
|
|
23483
|
+
function handoffFields(todo, goal) {
|
|
23484
|
+
const next = openItems(todo)[0]?.text || "";
|
|
23485
|
+
const blocked = goal?.status === "blocked" ? (goal.lastReason || "The goal is blocked.").slice(0, 500) : "";
|
|
23486
|
+
return {
|
|
23487
|
+
done: todo.filter((item) => item.status === "completed").slice(0, MAX_CHECKPOINT_DONE).map((item) => ({ item: item.text.slice(0, 300), evidence: [] })),
|
|
23488
|
+
next: next.slice(0, 500),
|
|
23489
|
+
...blocked ? { blocked } : {}
|
|
23490
|
+
};
|
|
23491
|
+
}
|
|
23492
|
+
var stopCheckpointReason = (outcome) => outcome.code === "needs_person" ? "requested" : "stuck";
|
|
23493
|
+
function stopSummary(outcome) {
|
|
23494
|
+
if (outcome.code === "needs_person") return `The reply stopped for the person: ${outcome.reason}`.slice(0, 1200);
|
|
23495
|
+
if (outcome.reason) return `The reply ended with work left: ${outcome.reason}`.slice(0, 1200);
|
|
23496
|
+
return `The reply ended with ${outcome.open.length} open todo item${outcome.open.length === 1 ? "" : "s"}.`;
|
|
23497
|
+
}
|
|
23498
|
+
|
|
23773
23499
|
// src/sessions/reasoningSummaries.ts
|
|
23774
23500
|
var MAX_REASONING_SUMMARY_CHARS = 4e3;
|
|
23775
23501
|
var REASONING_SUMMARY_APIS = /* @__PURE__ */ new Set([
|
|
@@ -24235,10 +23961,10 @@ var lastLine = (text10) => {
|
|
|
24235
23961
|
const lines2 = text10.split("\n").map((line2) => line2.trim()).filter(Boolean);
|
|
24236
23962
|
return (lines2.at(-1) ?? "").slice(0, 300);
|
|
24237
23963
|
};
|
|
24238
|
-
function derivedCheckpoint(ledger, slice, reason, fallback,
|
|
23964
|
+
function derivedCheckpoint(ledger, slice, reason, fallback, goal) {
|
|
24239
23965
|
return {
|
|
24240
23966
|
summary: ledger.lastText.trim().slice(0, MAX_CHECKPOINT_SUMMARY) || fallback,
|
|
24241
|
-
...handoffFields(
|
|
23967
|
+
...handoffFields(ledger.todo, goal),
|
|
24242
23968
|
files: [...ledger.files].slice(-MAX_CHECKPOINT_FILES).map(([path4, why]) => ({ path: path4.slice(0, 300), why: why.slice(0, 200) })),
|
|
24243
23969
|
checks: ledger.checks.slice(-MAX_CHECKPOINT_CHECKS),
|
|
24244
23970
|
decisions: [],
|
|
@@ -24261,7 +23987,7 @@ async function runTurn(run2, session, params) {
|
|
|
24261
23987
|
let costReported = false;
|
|
24262
23988
|
const toolStartedAt = /* @__PURE__ */ new Map();
|
|
24263
23989
|
const slice = params.slice?.state ?? null;
|
|
24264
|
-
const ledger = { files: /* @__PURE__ */ new Map(), checks: [], lastText: "" };
|
|
23990
|
+
const ledger = { files: /* @__PURE__ */ new Map(), checks: [], lastText: "", todo: [] };
|
|
24265
23991
|
const bashCommands = /* @__PURE__ */ new Map();
|
|
24266
23992
|
const heartbeat = slice ? setInterval(() => {
|
|
24267
23993
|
run2.emit("heartbeat", {
|
|
@@ -24459,6 +24185,10 @@ async function runTurn(run2, session, params) {
|
|
|
24459
24185
|
const command = tool.args && typeof tool.args === "object" ? tool.args.command : null;
|
|
24460
24186
|
if (typeof command === "string") bashCommands.set(tool.toolCallId, command);
|
|
24461
24187
|
}
|
|
24188
|
+
if (tool.toolName === TODO_WRITE) {
|
|
24189
|
+
const items = validatedTodo(tool.args && typeof tool.args === "object" ? tool.args.items : void 0);
|
|
24190
|
+
if (items) ledger.todo = items;
|
|
24191
|
+
}
|
|
24462
24192
|
break;
|
|
24463
24193
|
}
|
|
24464
24194
|
case "tool_execution_end": {
|
|
@@ -24633,14 +24363,14 @@ ${initialBashInstruction}` : prompt;
|
|
|
24633
24363
|
}
|
|
24634
24364
|
}
|
|
24635
24365
|
if (slice?.endedBy && !aborted && !lastError) {
|
|
24636
|
-
run2.emit("checkpoint", derivedCheckpoint(ledger, slice.slice, derivedReason(slice.endedBy), `The reply stopped after ${slice.steps} steps at its work-slice boundary.`, params.
|
|
24366
|
+
run2.emit("checkpoint", derivedCheckpoint(ledger, slice.slice, derivedReason(slice.endedBy), `The reply stopped after ${slice.steps} steps at its work-slice boundary.`, params.goal?.controller.get()));
|
|
24637
24367
|
run2.finish("interrupted", void 0, "slice_complete", slice.endedBy === "budget" ? "slice_budget" : "compaction_thrash");
|
|
24638
24368
|
return;
|
|
24639
24369
|
}
|
|
24640
24370
|
const goalState = params.goal?.controller.get() ?? null;
|
|
24641
|
-
const stop =
|
|
24371
|
+
const stop = goalState && !aborted && !lastError ? goalStopOutcome(ledger.todo, goalState) : null;
|
|
24642
24372
|
if (stop) {
|
|
24643
|
-
run2.emit("checkpoint", derivedCheckpoint(ledger, slice?.slice ?? 1, stopCheckpointReason(stop), stopSummary(stop),
|
|
24373
|
+
run2.emit("checkpoint", derivedCheckpoint(ledger, slice?.slice ?? 1, stopCheckpointReason(stop), stopSummary(stop), goalState));
|
|
24644
24374
|
run2.finish("interrupted", void 0, stop.code, stop.code);
|
|
24645
24375
|
return;
|
|
24646
24376
|
}
|
|
@@ -24879,21 +24609,6 @@ function checkCommand(command, historyFile, input) {
|
|
|
24879
24609
|
}
|
|
24880
24610
|
}
|
|
24881
24611
|
var CONTINUE_TEXT = "Continue from where you stopped.";
|
|
24882
|
-
function requestOfChain(root, runId) {
|
|
24883
|
-
const seen = /* @__PURE__ */ new Set();
|
|
24884
|
-
for (let current2 = runId; current2 && !seen.has(current2); ) {
|
|
24885
|
-
seen.add(current2);
|
|
24886
|
-
let record3;
|
|
24887
|
-
try {
|
|
24888
|
-
record3 = loadRun2(root, current2);
|
|
24889
|
-
} catch {
|
|
24890
|
-
return null;
|
|
24891
|
-
}
|
|
24892
|
-
if (record3.goal && record3.goal.trim() !== CONTINUE_TEXT) return record3.goal;
|
|
24893
|
-
current2 = record3.resumedFromRunId ?? null;
|
|
24894
|
-
}
|
|
24895
|
-
return null;
|
|
24896
|
-
}
|
|
24897
24612
|
function continueInput(root, input) {
|
|
24898
24613
|
const continuation = continuationOf(root, input.continueFrom, input.sessionDir);
|
|
24899
24614
|
if (!continuation.resumable || !continuation.resume) throw new HarnessError("continuation_unavailable", `Run ${input.continueFrom} cannot be continued (${continuation.reason ?? "history_missing"}).`);
|
|
@@ -25280,6 +24995,7 @@ Attached saved file references:`);
|
|
|
25280
24995
|
const own = [plan, resumed, profiles].filter((section2) => section2 !== null);
|
|
25281
24996
|
const effectiveLaunch = {
|
|
25282
24997
|
...launch,
|
|
24998
|
+
model: input.model ?? launch.model ?? null,
|
|
25283
24999
|
...sessionState ? { sessionState } : {},
|
|
25284
25000
|
sections: own.length ? [...(launch.sections ?? []).filter((section2) => !own.some((entry2) => entry2.id === section2.id)), ...own] : launch.sections,
|
|
25285
25001
|
tools: capabilities.tools,
|
|
@@ -25295,9 +25011,6 @@ Attached saved file references:`);
|
|
|
25295
25011
|
const planResearchChild = input.plan === true && input.run.relation === "subagent";
|
|
25296
25012
|
const sliceEnabled = slicePolicy.enabled && (harnessStateTools || planResearchChild);
|
|
25297
25013
|
const sliceState = createSliceState(slicePolicy, sliceOf(root, runId));
|
|
25298
|
-
const gateEnabled = harnessStateTools && !input.plan && !input.command && !input.automatic && !input.run.relation && !launchSwitchOff("XINGULAR_STOP_GATE");
|
|
25299
|
-
const originalRequest = input.text.trim() === CONTINUE_TEXT ? requestOfChain(root, previousRun?.id ?? null) : null;
|
|
25300
|
-
const stopGateState = gateEnabled ? createStopGateState(continuation?.todo) : null;
|
|
25301
25014
|
let goalController = null;
|
|
25302
25015
|
if (goalWanted && input.goal) {
|
|
25303
25016
|
goalController = new GoalController({ onChange: (state) => updateRun(root, runId, (record3) => {
|
|
@@ -25416,15 +25129,7 @@ ${continuationPrompt(goalState)}` : null;
|
|
|
25416
25129
|
...sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState, onRefuse: (event, reason) => {
|
|
25417
25130
|
if (typeof event.toolCallId === "string") run2.emit("tool.refused", { toolCallId: event.toolCallId, name: event.toolName, code: "work_slice", reason: reason.slice(0, 2e3) });
|
|
25418
25131
|
} }) }] : [],
|
|
25419
|
-
//
|
|
25420
|
-
...stopGateState ? [{ name: STOP_GATE_EXTENSION, factory: stopGate({
|
|
25421
|
-
state: stopGateState,
|
|
25422
|
-
initialTodo: continuation?.todo,
|
|
25423
|
-
request: () => originalRequest,
|
|
25424
|
-
sliceEnded: () => sliceEnabled && sliceState.endedBy !== null,
|
|
25425
|
-
onEvent: (event) => run2.emit("extension.notice", { kind: "notify", level: "info", extension: STOP_GATE_EXTENSION, text: stopGateNotice(event) })
|
|
25426
|
-
}) }] : [],
|
|
25427
|
-
// After the todo gate: a goal continues a reply the gate let end (ADR-0072).
|
|
25132
|
+
// A goal continues a reply the model ended (ADR-0072).
|
|
25428
25133
|
...goalController ? [{ name: GOAL_EXTENSION, factory: goalLoop({
|
|
25429
25134
|
controller: goalController,
|
|
25430
25135
|
check: launchSwitchOff("XINGULAR_COMPLETION_CHECK") ? null : sessionCheckModel,
|
|
@@ -25439,7 +25144,6 @@ ${continuationPrompt(goalState)}` : null;
|
|
|
25439
25144
|
}),
|
|
25440
25145
|
system,
|
|
25441
25146
|
...sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {},
|
|
25442
|
-
...stopGateState ? { stopGate: stopGateState } : {},
|
|
25443
25147
|
...goalController ? { goal: goalHandle } : {},
|
|
25444
25148
|
// A command sends no request of its own, so the context update waits for the next turn.
|
|
25445
25149
|
update: input.command ? null : update3,
|