@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/NOTICE.md +9 -7
  3. package/dist/cli.js +160 -456
  4. package/dist/sdk/prompt/compile.d.ts +4 -2
  5. package/dist/sdk/prompt/compile.js +10 -8
  6. package/dist/sdk/prompt/reporting.d.ts +2 -2
  7. package/dist/sdk/prompt/reporting.js +40 -250
  8. package/dist/sdk/prompt/standard.d.ts +4 -1
  9. package/dist/sdk/prompt/standard.js +6 -2
  10. package/dist/sdk/sessions/completionCheck.d.ts +4 -2
  11. package/dist/sdk/sessions/completionCheck.js +19 -1
  12. package/dist/sdk/sessions/contracts.js +2 -2
  13. package/dist/sdk/sessions/goalStop.d.ts +27 -0
  14. package/dist/sdk/sessions/goalStop.js +41 -0
  15. package/dist/sdk/sessions/runner.d.ts +0 -3
  16. package/dist/sdk/sessions/runner.js +16 -9
  17. package/dist/sdk/sessions/sessions.js +6 -36
  18. package/dist/sdk/sessions/workSlice.js +1 -1
  19. package/dist/sdk/terminal/piExtension.js +18 -31
  20. package/dist/sdk/terminal/terminals.js +1 -1
  21. package/dist/sdk/tools/askUser.js +1 -1
  22. package/dist/sdk/tools/todo.js +4 -3
  23. package/dist/terminal-opentui.js +108 -200
  24. package/dist/terminal-pi.js +262 -338
  25. package/docs/architecture/02-sessions-and-runs.md +13 -15
  26. package/docs/architecture/04-prompt.md +41 -35
  27. package/docs/architecture/07-tools-and-gate.md +2 -2
  28. package/docs/architecture/10-server-protocol.md +6 -5
  29. package/docs/architecture/11-terminal.md +7 -7
  30. package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +2 -2
  31. package/docs/decisions/ADR-0069-stop-gate-and-finish.md +8 -2
  32. package/docs/decisions/ADR-0070-terminal-slice-continuation.md +4 -1
  33. package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +3 -0
  34. package/docs/decisions/ADR-0073-large-reporting-prompt.md +6 -1
  35. package/docs/decisions/ADR-0074-report-content-not-report-blocks.md +90 -0
  36. package/docs/decisions/ADR-0075-no-todo-gate.md +84 -0
  37. package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +93 -0
  38. package/docs/releases/1.4.0-unstable.md +9 -2
  39. package/docs/releases/1.5.0-unstable.md +23 -0
  40. package/docs/study/STUDY-0004-turn-end-controls.md +7 -0
  41. package/docs/study/STUDY-0006-reporting-prompts.md +48 -10
  42. package/package.json +1 -1
  43. package/dist/sdk/sessions/stopGate.d.ts +0 -78
  44. 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.4.0-unstable",
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
- /** The completion check found that the work left needs the person (ADR-0071). */
791
+ /** A goal stopped for the person (ADR-0072). */
792
792
  "needs_person",
793
- /** The reply ended with work left after the stop gate's nudges ran out or produced no work (ADR-0069, ADR-0071). */
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 or will take more than one tool call, 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, list the steps of a handoff or plan instead of folding them into one "remaining work" item, and never stop because the list is short or done. Keep the list true at all times: mark a step completed only once it is done and checked, and never mark unfinished work completed. An open step is work to do in this reply, not something to report. Before you end the turn, compare the request, not the list, with what is done and checked, and do whatever is still yours to do. The person follows your work from it, and a subagent\u2019s todo list shows as its progress in the person\u2019s list of subagents.';
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, state the assumption you made, and keep working on whatever does not depend on the answer.
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 say exactly what is needed; otherwise the reply ends only when the work-slice guard hands it back or the person interrupts.`;
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 = `When you finish work, write the final reply for a person who did not watch you work. They have not seen your tool calls, the files you read, or the output you saw. The reply is the whole report: it must stand alone. Reporting is for work that is finished or genuinely blocked. Do not end a turn to report work you can still do.
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
- ### What the reply is for
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
- The person reads the reply to decide what to do next. Give them, in this order: what happened, what changed, how to try it, and what they should do. Everything else is optional.
2570
+ ### Technical communication
2568
2571
 
2569
- Write for a competent colleague who was not in the session. Do not assume they know the plan, the files, the commands, or the attempts that failed. If they would have to ask a question to act on the reply, the reply is not finished.
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
- The reply is not a transcript. Leave out what you tried and abandoned, the tool calls, your internal reasoning, and code the person can open. Keep what changes their decision or their next step. A person often reads only the first line and the test block, so both must stand alone.
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
- ### The first line
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
- - State the outcome, not the activity. Write "The importer now handles quoted fields; all 12 tests pass", not "I worked on the importer".
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
- ### Choose the shape
2580
+ ### Headings
2581
2581
 
2582
- | The task was | The shape |
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
- #### Beyond the request
2584
+ ### Shape by work
2647
2585
 
2648
- When you did something the request did not name, list each piece and why the work needed it. Examples: a test that would have caught the bug, a fix to the adjacent broken call, a doc line that would otherwise be wrong. This block makes the extra work auditable; it is not a place to list routine steps.
2649
-
2650
- #### Departures from the plan
2651
-
2652
- When you departed from a plan, a handoff or the steps the request gave, say what you did instead and why, and what effect it has. This includes a skipped step, a reordered step, a replaced tool, a narrower scope or a wider one. Do not hide a departure inside the summary.
2653
-
2654
- #### Unverified or needs a person
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
- - A change: about 20 to 60 lines. A large change may need more, but the outcome, the test block and the next step come first and stay short.
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
- ### Tone
2599
+ ### Prose and lists
2710
2600
 
2711
- Write directly and concretely. No apologies, no self-congratulation, no filler. Technical detail belongs in the reply when it helps the person act; leave out the reasoning that only mattered while you worked. If the person's language is not English, reply in their language; this guidance describes the shape, not the language.
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. Copy the structure and the level of detail, then write for the task in front of you.
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
- I ran \`npm run test:export\`: 18 passed. I did not test the disconnected-network case in the app;
2773
- that is the manual step above.
2607
+ #### Example 1 \u2014 a screen change
2774
2608
 
2775
- ## Beyond the request
2609
+ The settings page now remembers the last selected tab, and the choice survives a reload.
2776
2610
 
2777
- I added the error field to the job record. The retry alone would have hidden the cause, and the
2778
- field is what the next attempt reads.
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
- Nothing needed next.
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 3 \u2014 a blocked or unfinished change
2618
+ #### Example 2 \u2014 a blocked publish
2783
2619
 
2784
- Not finished: the sync path is implemented and tested, but the API key is missing, so I could not
2785
- run it against the live service.
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
- ## What is done
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
- - A change has a what-changed block, a how-to-test block and a what-to-do-next block.
2833
- - Paths are named; a table holds three or more files.
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 = 18e3;
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 STOP_GATE_GUIDANCE = "A reply that ends while its todo list has open items is sent back to you, so keep working until the request is done. 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.";
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 nativeGuidance = `Files and shell: ${readGuidance} ${writeGuidance} Edits and commands still pass through the Harness gate.`;
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 gate = launch.standard ? STOP_GATE_GUIDANCE : "";
2972
- groups.push({ group: "Progress", tools: progressTools, order: 87, guidance: `Progress: ${[TODO_GUIDANCE, gate].filter(Boolean).join(" ")}` });
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, gate, goal) {
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(gate, goal),
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.stopGate, params.goal?.controller.get()));
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 = (params.stopGate || goalState) && !aborted && !lastError ? stopOutcome(params.stopGate ?? createStopGateState(), goalState) : null;
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), params.stopGate, goalState));
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
- // Last, so delivered subagent results and plan repair continue first and the gate sees them.
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,