pi-orchestrator-config 4.8.4 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -8
- package/agents/code-reviewer-docs.md +3 -3
- package/agents/git-expert.md +1 -1
- package/extensions/pidiff/pidiff-ui/package.json +1 -1
- package/extensions/providers/index.ts +17 -41
- package/extensions/providers/model-restore-wiring.ts +120 -0
- package/extensions/providers/restore-default-model.ts +61 -21
- package/package.json +1 -1
- package/prompts/create-commit.md +58 -0
- package/prompts/review-handler.md +57 -33
- package/rules/20-code-review-loop.md +1 -1
- package/rules/25-documentation-updates.md +2 -2
- package/templates/commit-prompt.md +38 -0
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ Single extension that provides:
|
|
|
14
14
|
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
15
|
| **Subagent tool** | Delegate tasks to specialist agents (single, parallel, chain, async modes); optional `model` param for model override |
|
|
16
16
|
| **`list_models`** | List available models and providers for subagent model override |
|
|
17
|
-
| **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`: `x` kills, `Space` multi-selects, `a` kills all). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `
|
|
18
|
-
| **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`new
|
|
17
|
+
| **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`: `x` kills, `Space` multi-selects, `a` kills all). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `docs/cli-provider.md`) |
|
|
18
|
+
| **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`resume` (`new` only when no model is selected), empty `enabledModels`, trusted project merge — see `docs/cli-provider.md`. Headless Cursor `--approve-mcps` when `CLI_APPROVE_MCPS` is set (`false` opts out) or the process is a sidecar (`SIDECAR_PORT`; `startSidecar()` stamps it while running and restores it on `close()`). Executable sidecar consumers should call `bindSidecarListenExit()` so bind/`stopped` fatals exit 1. Headless Gemini defaults `GEMINI_CLI_TRUST_WORKSPACE=true` and preserves an explicit parent value (including `false`). |
|
|
19
19
|
| **`/btw` command** | Quick side questions without polluting conversation history — ephemeral overlay |
|
|
20
20
|
| **`/async-status` command** | Show status of background agents — select one for live output streaming, multi-select and kill (`x`), or kill everything (`a`) |
|
|
21
21
|
| **`ask_user` tool** | Structured user input with options and free-text — used by workflows |
|
|
@@ -75,6 +75,7 @@ for `git-expert` to commit.
|
|
|
75
75
|
| `/refine-review <url>` | Refine and improve existing PR review comments |
|
|
76
76
|
| `/coderabbit-rate-limit [number\|url]` | Handle CodeRabbit rate limiting on PRs |
|
|
77
77
|
| `/create-coms-feature-manager` | Generate a coms feature-manager prompt customized for the current project (source template: `templates/coms-feature-manager-prompt.md`) |
|
|
78
|
+
| `/create-commit <message>` | Commit and push the current change set via git-expert (source template: `templates/commit-prompt.md` — stage exact files, commit, push, report sha) |
|
|
78
79
|
| `/query-db <command>` | Query the review comments database |
|
|
79
80
|
| `/external-ai <agent> [--model <model>] [--session-id <id>] [--fix\|--peer\|--resume] <prompt>` | Run prompts via AI CLIs directly (cursor, claude, gemini) — full model access |
|
|
80
81
|
| `/external-ai-models-refresh` | Refresh cached AI CLI models for autocomplete |
|
|
@@ -305,7 +306,7 @@ See [`pi-config-settings.example.jsonc`](pi-config-settings.example.jsonc) for a
|
|
|
305
306
|
|
|
306
307
|
Resolution order: project file → global `~/.pi/pi-config-settings.json` → env var → default.
|
|
307
308
|
|
|
308
|
-
See `
|
|
309
|
+
See `docs/configuration.md` for the full settings table.
|
|
309
310
|
|
|
310
311
|
#### Reviewer Environment Variables
|
|
311
312
|
|
|
@@ -368,7 +369,7 @@ Rules auto-load alphabetically. Same-filename entries are overridden (project >
|
|
|
368
369
|
|
|
369
370
|
Rules support settings/feature conditionals at prompt assembly (`assembleRuleText` in
|
|
370
371
|
`rule-placeholders.ts`). Full syntax, truthiness, and assembly order:
|
|
371
|
-
**[
|
|
372
|
+
**[docs/rules-and-layers.md](docs/rules-and-layers.md)** (Rules assembly).
|
|
372
373
|
|
|
373
374
|
Markers: `{{IF:key}}…{{/IF}}`, `{{IFNOT:key}}…{{/IFNOT}}`, comparisons
|
|
374
375
|
`{{IF:key==value}}` / `{{IF:key!=value}}`. Whole-file frontmatter: `requires_setting` /
|
|
@@ -483,7 +484,7 @@ that directory. Do not stop running containers merely to test this PR.
|
|
|
483
484
|
After register, `filterModels` hides models when unavailable: PATH cleared while
|
|
484
485
|
agent state remains, restore PATH; after `session_shutdown` (AgentState cleared
|
|
485
486
|
and `initialized` reset) the next factory re-registers on `/new`|`/resume`|`/fork`
|
|
486
|
-
(PATH alone is not enough). See `
|
|
487
|
+
(PATH alone is not enough). See `docs/cli-provider.md`.
|
|
487
488
|
|
|
488
489
|
**ACPX provider agents (optional `acpx_agents`):** ACPX wraps CLI agents via the
|
|
489
490
|
ACP runtime. Supported agents: `cursor`, `claude`, `gemini`. Enable with
|
|
@@ -500,10 +501,10 @@ symlinks package `agents/*.md` into the mounted project:
|
|
|
500
501
|
|
|
501
502
|
Uses `ln -sfn` (safe if multiple `pi-docker` sessions share the same folder). Those three
|
|
502
503
|
directories are added to the container global gitignore. **Native** installs do not auto-
|
|
503
|
-
sync — copy or symlink yourself (see `
|
|
504
|
+
sync — copy or symlink yourself (see `docs/cli-provider.md`).
|
|
504
505
|
|
|
505
506
|
**Extension ops logs:** `cli-provider` and dreaming write to `~/.pi/logs/` (not the chat
|
|
506
|
-
UI). Fallback: `$TMPDIR/pi-logs/`. Details in `
|
|
507
|
+
UI). Fallback: `$TMPDIR/pi-logs/`. Details in `docs/cli-provider.md` (Logging).
|
|
507
508
|
|
|
508
509
|
### Pre-built image
|
|
509
510
|
|
|
@@ -767,12 +768,30 @@ PI_PIDIFF_ENABLE=false pi
|
|
|
767
768
|
| `myk-pi-tools pr claude-md` | Fetch `CLAUDE.md` and `AGENTS.md` content for a PR's repository |
|
|
768
769
|
| `myk-pi-tools release create` | Create a GitHub release with changelog |
|
|
769
770
|
| `myk-pi-tools db` | Review database query commands |
|
|
770
|
-
| `myk-pi-tools reviews` | Review handling commands
|
|
771
|
+
| `myk-pi-tools reviews` | Review handling commands (fetch, poll, address, post, store) |
|
|
772
|
+
| `myk-pi-tools reviews address` | Apply replies/statuses to a fetched reviews JSON, then post and store it (see below) |
|
|
771
773
|
| `myk-pi-tools memory` | Project memory commands — persistent per-repo learning |
|
|
772
774
|
| `myk-pi-tools ai-cli` | AI CLI commands (cursor, claude, gemini) |
|
|
773
775
|
| `myk-pi-tools coderabbit` | CodeRabbit commands (check, trigger, retry) |
|
|
774
776
|
| `myk-pi-tools settings get [key ...]` | Resolve settings (project → global → env → default). No args = all keys |
|
|
775
777
|
|
|
778
|
+
`reviews address` — bulk-apply replies to a fetched reviews JSON, then post and store it:
|
|
779
|
+
|
|
780
|
+
```bash
|
|
781
|
+
myk-pi-tools reviews address [--json <reviews.json>] [--replies <mapping.txt>] [--set <status>]
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
- `--json`: reviews JSON to process (default: newest `pr-*-reviews.json` in `.pi/tmp`)
|
|
785
|
+
- `--replies`: mapping file, one rule per line — `<path>:<line>|<reply>` or `*|<reply>` (matches all pending findings);
|
|
786
|
+
`#` comments and blank lines ignored
|
|
787
|
+
- `--set`: target status for matched findings — `addressed` (default), `skipped`, `not_addressed`, or `failed`
|
|
788
|
+
- Without `--replies`, pending findings that already carry a reply only get their status flipped (status-only mode)
|
|
789
|
+
- Exit code `0` = replies posted and stored; non-zero = nothing matched, validation failed, or posting failed
|
|
790
|
+
(the JSON is kept for retry — re-running with the same or a corrected mapping resumes unposted entries)
|
|
791
|
+
- Validation failures change nothing: a rule matching several distinct findings at one location, or a status `post`
|
|
792
|
+
rejects for Qodo sticky findings (`not_addressed`/`failed`; `skipped` only with an issue-spec-update reply),
|
|
793
|
+
exits non-zero before the JSON is saved
|
|
794
|
+
|
|
776
795
|
### What's protected
|
|
777
796
|
|
|
778
797
|
**Filesystem isolation** — the container cannot access anything outside the mounted volumes:
|
|
@@ -917,6 +936,10 @@ CLI commands: `npx pi-sidecar` (start server), `npx pi-sidecar-start` (backgroun
|
|
|
917
936
|
|
|
918
937
|
`GET /providers` lists registered providers and whether each supports a session API key, even when no models or credentials are available. The Python client exposes this as `await SidecarClient.get_providers()`.
|
|
919
938
|
|
|
939
|
+
The sidecar's internal agent dir is configurable (startSidecar `agentDir` option / `PI_SIDECAR_AGENT_DIR`, default `/tmp/pi-sidecar-agent`) so
|
|
940
|
+
deployments can supply custom providers, auth, and settings — see
|
|
941
|
+
[`packages/pi-sidecar/README.md`](packages/pi-sidecar/README.md).
|
|
942
|
+
|
|
920
943
|
See [`packages/pi-sidecar/README.md`](packages/pi-sidecar/README.md) for full documentation.
|
|
921
944
|
See [`packages/pi-sidecar/CONSUMER-GUIDE.md`](packages/pi-sidecar/CONSUMER-GUIDE.md) for integration best practices.
|
|
922
945
|
|
|
@@ -76,8 +76,8 @@ Use this documentation updates table:
|
|
|
76
76
|
| Change | Check these files |
|
|
77
77
|
|--------|-------------------|
|
|
78
78
|
| New feature/command/tool | `README.md` (feature table, usage examples) |
|
|
79
|
-
| New or modified extension module | `
|
|
80
|
-
| New agent added/removed | `
|
|
79
|
+
| New or modified extension module | `contributing/repo-structure.md` (repository structure) |
|
|
80
|
+
| New agent added/removed | `contributing/repo-structure.md`, routing rules, bug reporting agent list |
|
|
81
81
|
| New prompt template | `README.md` (prompt templates table) |
|
|
82
82
|
| Docker/container changes | `README.md` (Docker section), `Dockerfile` |
|
|
83
83
|
| New CLI tool or dependency | `README.md` (tools table), `Dockerfile` |
|
|
@@ -119,7 +119,7 @@ Flag audit failures as `[WARNING]` with the specific checklist item that failed.
|
|
|
119
119
|
|
|
120
120
|
### 5. Cross-File Consistency
|
|
121
121
|
|
|
122
|
-
- Do README.md, AGENTS.md, and
|
|
122
|
+
- Do README.md, AGENTS.md, and contributing/ agree on commands, structure, and workflows?
|
|
123
123
|
- Are version numbers, feature lists, and agent counts consistent?
|
|
124
124
|
- Do referenced files actually exist?
|
|
125
125
|
|
package/agents/git-expert.md
CHANGED
|
@@ -135,7 +135,7 @@ Format rules:
|
|
|
135
135
|
**Commit changes:**
|
|
136
136
|
|
|
137
137
|
1. `git status` to see changes
|
|
138
|
-
2. `git add <specific files>` for each file (NEVER `git add
|
|
138
|
+
2. `git add -- <specific files>` for each file (NEVER `git add .`; the `--` terminator prevents paths being read as options)
|
|
139
139
|
3. Commit with proper format
|
|
140
140
|
4. Report the result
|
|
141
141
|
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
isProvidersInitialized,
|
|
68
68
|
markProvidersInitialized,
|
|
69
69
|
} from "./initialized-guard.js";
|
|
70
|
-
import {
|
|
70
|
+
import { registerModelRestoreOnSessionStart } from "./model-restore-wiring.js";
|
|
71
71
|
import { registerLastThinkingLevel } from "./last-thinking-level.js";
|
|
72
72
|
import { teardownProvidersOnSessionShutdown } from "./session-shutdown.js";
|
|
73
73
|
|
|
@@ -631,6 +631,22 @@ export default async function (
|
|
|
631
631
|
}
|
|
632
632
|
};
|
|
633
633
|
|
|
634
|
+
// Cold-start default model restore (#753), wired as its own session_start
|
|
635
|
+
// handler (multiple registrations are fine) so the wiring is unit-testable.
|
|
636
|
+
// Registered before the discovery-summary handler; the restore itself is
|
|
637
|
+
// fire-and-forget so ordering with the summary/thinking handler is benign.
|
|
638
|
+
registerModelRestoreOnSessionStart(pi, {
|
|
639
|
+
argv: testOptions?.argv,
|
|
640
|
+
onRestoreModel: (model, setSelected) => {
|
|
641
|
+
log.debug("onRestoreModel handoff", { provider: model.provider, id: model.id });
|
|
642
|
+
return lastThinking.restoreModel(model as Model<any>, setSelected);
|
|
643
|
+
},
|
|
644
|
+
onRestoreSettled: (event, ctx, modelRestore) => {
|
|
645
|
+
log.debug("onRestoreSettled handoff", { reason: (event as { reason?: string })?.reason });
|
|
646
|
+
return void lastThinking.applyAfterModelRestore(event, ctx, modelRestore);
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
|
|
634
650
|
pi.on("session_start", (event, ctx) => {
|
|
635
651
|
const reason = typeof event?.reason === "string" ? event.reason : "";
|
|
636
652
|
const summary = `Providers: ${providerSummaryParts.join(", ")}`;
|
|
@@ -651,46 +667,6 @@ export default async function (
|
|
|
651
667
|
? { id: ctx.model.id, provider: String(ctx.model.provider) }
|
|
652
668
|
: undefined,
|
|
653
669
|
);
|
|
654
|
-
// agentDir via PI_CODING_AGENT_DIR / ~/.pi/agent only (ctx has cwd, not agentDir).
|
|
655
|
-
// Merge global + cwd/.pi/settings.json (project wins) only when trusted —
|
|
656
|
-
// same gate as pi SettingsManager (ctx.isProjectTrusted). Fail closed.
|
|
657
|
-
const cwd = typeof ctx.cwd === "string" ? ctx.cwd : undefined;
|
|
658
|
-
let projectTrusted = false;
|
|
659
|
-
try {
|
|
660
|
-
projectTrusted = typeof ctx.isProjectTrusted === "function"
|
|
661
|
-
? ctx.isProjectTrusted() === true
|
|
662
|
-
: false;
|
|
663
|
-
} catch (err) {
|
|
664
|
-
log.debug(
|
|
665
|
-
"restore-default-model isProjectTrusted failed",
|
|
666
|
-
err instanceof Error ? err.message : String(err),
|
|
667
|
-
);
|
|
668
|
-
projectTrusted = false;
|
|
669
|
-
}
|
|
670
|
-
const modelRestore = restoreDefaultModelOnSessionStart({
|
|
671
|
-
ctx: {
|
|
672
|
-
model: ctx.model
|
|
673
|
-
? { id: ctx.model.id, provider: String(ctx.model.provider) }
|
|
674
|
-
: undefined,
|
|
675
|
-
modelRegistry: ctx.modelRegistry,
|
|
676
|
-
},
|
|
677
|
-
reason,
|
|
678
|
-
cwd,
|
|
679
|
-
projectTrusted,
|
|
680
|
-
argv: testOptions?.argv,
|
|
681
|
-
getCurrentModel: () =>
|
|
682
|
-
ctx.model
|
|
683
|
-
? { id: ctx.model.id, provider: String(ctx.model.provider) }
|
|
684
|
-
: undefined,
|
|
685
|
-
setModel: (model) => lastThinking.restoreModel(model as Model<any>, (selected) => pi.setModel(selected)),
|
|
686
|
-
}).catch((err) => {
|
|
687
|
-
log.warn(
|
|
688
|
-
"restore-default-model session_start error",
|
|
689
|
-
err instanceof Error ? err.message : String(err),
|
|
690
|
-
);
|
|
691
|
-
return false;
|
|
692
|
-
});
|
|
693
|
-
void lastThinking.applyAfterModelRestore(event, ctx, modelRestore);
|
|
694
670
|
});
|
|
695
671
|
|
|
696
672
|
pi.on("model_select", (event) => {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session_start wiring for the cold-start default model restore (#753).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from extensions/providers/index.ts so the handler wiring —
|
|
5
|
+
* reason forwarding, setModel plumbing, cwd/projectTrusted extraction,
|
|
6
|
+
* fire-and-forget .catch, applyAfterModelRestore handoff — can be unit
|
|
7
|
+
* tested with a fake pi object. A wiring bug here (wrong reason, broken
|
|
8
|
+
* setModel) would otherwise reset the model undetected (#901 finding).
|
|
9
|
+
*
|
|
10
|
+
* Kept free of @earendil-works/pi-ai imports beyond the Model type so unit
|
|
11
|
+
* tests can import under tsx.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { createLogger } from "../shared/logger.js";
|
|
17
|
+
import { restoreDefaultModelOnSessionStart, type ModelLike } from "./restore-default-model.js";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("providers");
|
|
20
|
+
|
|
21
|
+
/** Raw selection call (pi.setModel) handed to onRestoreModel. */
|
|
22
|
+
export type ModelRestoreSetModel = (model: ModelLike) => Promise<boolean>;
|
|
23
|
+
|
|
24
|
+
export type RegisterModelRestoreOnSessionStartOpts = {
|
|
25
|
+
/** CLI argv to scan for --model / --provider / --models (user override). */
|
|
26
|
+
argv?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* setModel wrapper invoked by the restore. Receives the resolved default
|
|
29
|
+
* model and a raw setSelected that performs the actual selection
|
|
30
|
+
* (pi.setModel). Defaults to calling setSelected directly. index.ts passes
|
|
31
|
+
* lastThinking.restoreModel so thinking preferences follow the switch.
|
|
32
|
+
*/
|
|
33
|
+
onRestoreModel?: (
|
|
34
|
+
model: ModelLike,
|
|
35
|
+
setSelected: ModelRestoreSetModel,
|
|
36
|
+
) => Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Hook receiving the fire-and-forget (already caught) restore promise —
|
|
39
|
+
* index.ts passes lastThinking.applyAfterModelRestore so the saved thinking
|
|
40
|
+
* level is applied once the model restore settles. Optional.
|
|
41
|
+
*/
|
|
42
|
+
onRestoreSettled?: (
|
|
43
|
+
event: any,
|
|
44
|
+
ctx: any,
|
|
45
|
+
modelRestore: Promise<boolean>,
|
|
46
|
+
) => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register a session_start handler that restores the saved default model
|
|
51
|
+
* when the #753 gates pass (see restore-default-model.ts). Fire-and-forget:
|
|
52
|
+
* session_start is not blocked by retries.
|
|
53
|
+
*/
|
|
54
|
+
export function registerModelRestoreOnSessionStart(
|
|
55
|
+
pi: ExtensionAPI,
|
|
56
|
+
opts: RegisterModelRestoreOnSessionStartOpts = {},
|
|
57
|
+
): void {
|
|
58
|
+
const onRestoreModel =
|
|
59
|
+
opts.onRestoreModel ?? ((model, setSelected) => setSelected(model));
|
|
60
|
+
log.debug("model-restore-wiring: registering session_start handler");
|
|
61
|
+
|
|
62
|
+
pi.on("session_start", (event, ctx) => {
|
|
63
|
+
const reason = typeof event?.reason === "string" ? event.reason : "";
|
|
64
|
+
log.debug("model-restore-wiring session_start", { reason });
|
|
65
|
+
|
|
66
|
+
// agentDir via PI_CODING_AGENT_DIR / ~/.pi/agent only (ctx has cwd, not agentDir).
|
|
67
|
+
// Merge global + cwd/.pi/settings.json (project wins) only when trusted —
|
|
68
|
+
// same gate as pi SettingsManager (ctx.isProjectTrusted). Fail closed.
|
|
69
|
+
const cwd = typeof ctx.cwd === "string" ? ctx.cwd : undefined;
|
|
70
|
+
let projectTrusted = false;
|
|
71
|
+
try {
|
|
72
|
+
projectTrusted = typeof ctx.isProjectTrusted === "function"
|
|
73
|
+
? ctx.isProjectTrusted() === true
|
|
74
|
+
: false;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.debug(
|
|
77
|
+
"restore-default-model isProjectTrusted failed",
|
|
78
|
+
{
|
|
79
|
+
cwd,
|
|
80
|
+
reason,
|
|
81
|
+
error: err instanceof Error ? err.message : String(err),
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
projectTrusted = false;
|
|
85
|
+
}
|
|
86
|
+
// Fire-and-forget restore so session_start is not blocked by retries (#753).
|
|
87
|
+
// Omit registeredProviders: cli/acpx-only lists falsely fail-fast native defaults.
|
|
88
|
+
const modelRestore = restoreDefaultModelOnSessionStart({
|
|
89
|
+
ctx: {
|
|
90
|
+
model: ctx.model
|
|
91
|
+
? { id: ctx.model.id, provider: String(ctx.model.provider) }
|
|
92
|
+
: undefined,
|
|
93
|
+
modelRegistry: ctx.modelRegistry,
|
|
94
|
+
},
|
|
95
|
+
reason,
|
|
96
|
+
cwd,
|
|
97
|
+
projectTrusted,
|
|
98
|
+
argv: opts.argv,
|
|
99
|
+
getCurrentModel: () =>
|
|
100
|
+
ctx.model
|
|
101
|
+
? { id: ctx.model.id, provider: String(ctx.model.provider) }
|
|
102
|
+
: undefined,
|
|
103
|
+
setModel: (model) =>
|
|
104
|
+
onRestoreModel(model, (selected) =>
|
|
105
|
+
pi.setModel(selected as Model<any>),
|
|
106
|
+
),
|
|
107
|
+
}).catch((err) => {
|
|
108
|
+
log.warn(
|
|
109
|
+
"restore-default-model session_start error",
|
|
110
|
+
err instanceof Error ? err.message : String(err),
|
|
111
|
+
);
|
|
112
|
+
return false;
|
|
113
|
+
});
|
|
114
|
+
if (opts.onRestoreSettled) {
|
|
115
|
+
opts.onRestoreSettled(event, ctx, modelRestore);
|
|
116
|
+
} else {
|
|
117
|
+
void modelRestore;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* On session_start (providers already registered), restore the saved default
|
|
8
8
|
* when current differs (or is missing), unless the user passed --model /
|
|
9
9
|
* --provider / --models, enabledModels scopes the model list, or the session
|
|
10
|
-
* is fork/reload.
|
|
10
|
+
* is fork/reload. "/new" keeps a selected model but still fills an empty
|
|
11
|
+
* selection (#901).
|
|
11
12
|
*
|
|
12
13
|
* Kept free of @earendil-works/pi-ai so unit tests can import under tsx.
|
|
13
14
|
*/
|
|
@@ -20,14 +21,17 @@ import { createLogger } from "../shared/logger.js";
|
|
|
20
21
|
const log = createLogger("providers");
|
|
21
22
|
|
|
22
23
|
/** session_start reasons where cold-start restore is allowed. */
|
|
23
|
-
export const RESTORE_ALLOWED_REASONS = new Set(["startup", "
|
|
24
|
+
export const RESTORE_ALLOWED_REASONS = new Set(["startup", "resume"]);
|
|
24
25
|
|
|
25
26
|
export type RestoreDefaultModelOpts = {
|
|
26
27
|
defaultProvider?: string | null;
|
|
27
28
|
defaultModelId?: string | null;
|
|
28
29
|
currentProvider?: string | null;
|
|
29
30
|
currentModelId?: string | null;
|
|
30
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* session_start reason; "startup" | "resume" may restore, and "new" only
|
|
33
|
+
* when no model is currently selected (empty selection is filled).
|
|
34
|
+
*/
|
|
31
35
|
reason?: string | null;
|
|
32
36
|
/**
|
|
33
37
|
* CLI argv to scan for --model / --provider / --models (user override).
|
|
@@ -78,14 +82,33 @@ export function hasEnabledModelsScope(
|
|
|
78
82
|
*
|
|
79
83
|
* Gates (all must pass):
|
|
80
84
|
* 1. defaultProvider and defaultModelId both non-empty
|
|
81
|
-
* 2. reason is startup|new
|
|
85
|
+
* 2. reason is startup|resume; "new" is allowed only when the current model
|
|
86
|
+
* is missing (an existing selection is kept — nothing to preserve when
|
|
87
|
+
* empty). fork/reload never restore.
|
|
82
88
|
* 3. current missing OR current provider/id ≠ default
|
|
83
|
-
* 4. argv does not contain --model, --provider, or --models
|
|
84
|
-
*
|
|
89
|
+
* 4. argv does not contain --model, --provider, or --models (blocks in all
|
|
90
|
+
* cases, including "new" with a missing model)
|
|
91
|
+
* 5. enabledModels is missing or empty (non-empty scopes like --models;
|
|
92
|
+
* blocks in all cases)
|
|
85
93
|
*/
|
|
86
94
|
export function shouldRestoreDefaultModel(opts: RestoreDefaultModelOpts): boolean {
|
|
87
95
|
const reason = typeof opts.reason === "string" ? opts.reason.trim() : "";
|
|
88
|
-
|
|
96
|
+
|
|
97
|
+
const currentProvider = typeof opts.currentProvider === "string"
|
|
98
|
+
? opts.currentProvider.trim()
|
|
99
|
+
: "";
|
|
100
|
+
const currentModelId = typeof opts.currentModelId === "string"
|
|
101
|
+
? opts.currentModelId.trim()
|
|
102
|
+
: "";
|
|
103
|
+
const currentMissing = !currentProvider || !currentModelId;
|
|
104
|
+
|
|
105
|
+
// Reason gate: startup|resume always eligible; "new" only when no model is
|
|
106
|
+
// selected (fills an empty selection; an existing selection is kept, #901).
|
|
107
|
+
if (
|
|
108
|
+
!RESTORE_ALLOWED_REASONS.has(reason)
|
|
109
|
+
&& !(reason === "new" && currentMissing)
|
|
110
|
+
) {
|
|
111
|
+
log.debug("shouldRestoreDefaultModel reject", { stage: "reason-gate", reason, currentMissing });
|
|
89
112
|
return false;
|
|
90
113
|
}
|
|
91
114
|
|
|
@@ -96,25 +119,26 @@ export function shouldRestoreDefaultModel(opts: RestoreDefaultModelOpts): boolea
|
|
|
96
119
|
? opts.defaultModelId.trim()
|
|
97
120
|
: "";
|
|
98
121
|
|
|
99
|
-
if (!provider || !modelId)
|
|
122
|
+
if (!provider || !modelId) {
|
|
123
|
+
log.debug("shouldRestoreDefaultModel reject", { stage: "missing-defaults", reason });
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
100
126
|
|
|
101
127
|
if (argvHasModelOrProviderOverride(opts.argv)) {
|
|
128
|
+
log.debug("shouldRestoreDefaultModel reject", { stage: "argv-override", reason });
|
|
102
129
|
return false;
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
if (hasEnabledModelsScope(opts.enabledModels)) {
|
|
133
|
+
log.debug("shouldRestoreDefaultModel reject", { stage: "enabled-models-scope", reason });
|
|
106
134
|
return false;
|
|
107
135
|
}
|
|
108
136
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
: "";
|
|
115
|
-
|
|
116
|
-
if (!currentProvider || !currentModelId) return true;
|
|
117
|
-
return currentProvider !== provider || currentModelId !== modelId;
|
|
137
|
+
const decision = currentMissing
|
|
138
|
+
|| currentProvider !== provider
|
|
139
|
+
|| currentModelId !== modelId;
|
|
140
|
+
log.debug("shouldRestoreDefaultModel decision", { reason, decision });
|
|
141
|
+
return decision;
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
export type PiAgentDefaults = {
|
|
@@ -425,6 +449,10 @@ export async function restoreDefaultModelOnSessionStart(opts: {
|
|
|
425
449
|
const currentProvider = current?.provider;
|
|
426
450
|
const currentModelId = current?.id;
|
|
427
451
|
const argv = opts.argv ?? process.argv;
|
|
452
|
+
// Normalize once: the decision gate trims the reason, so the mid-flight
|
|
453
|
+
// abort guard must compare the trimmed value too — a padded reason (e.g.
|
|
454
|
+
// " new ") must not split the two code paths.
|
|
455
|
+
const reason = typeof opts.reason === "string" ? opts.reason.trim() : "";
|
|
428
456
|
const argvOverride = argvHasModelOrProviderOverride(argv);
|
|
429
457
|
const enabledModelsScope = hasEnabledModelsScope(enabledModels);
|
|
430
458
|
|
|
@@ -439,7 +467,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
|
|
|
439
467
|
})) {
|
|
440
468
|
log.debug(
|
|
441
469
|
"restore-default-model skip",
|
|
442
|
-
`reason=${
|
|
470
|
+
`reason=${reason || "?"} default=${defaultProvider ?? "?"}/${defaultModel ?? "?"} ` +
|
|
443
471
|
`current=${currentProvider ?? "?"}/${currentModelId ?? "?"} ` +
|
|
444
472
|
`argvOverride=${argvOverride} enabledModelsScope=${enabledModelsScope}` +
|
|
445
473
|
(enabledModelsScope ? ` enabledModels=${enabledModels!.length}` : ""),
|
|
@@ -473,7 +501,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
|
|
|
473
501
|
log.warn(
|
|
474
502
|
"restore-default-model fail-fast",
|
|
475
503
|
`registeredProviders lack ${defaultProvider} (count=${count}): ${label} ` +
|
|
476
|
-
`(session_start reason=${
|
|
504
|
+
`(session_start reason=${reason || "?"})`,
|
|
477
505
|
);
|
|
478
506
|
return false;
|
|
479
507
|
}
|
|
@@ -503,6 +531,18 @@ export async function restoreDefaultModelOnSessionStart(opts: {
|
|
|
503
531
|
return true;
|
|
504
532
|
}
|
|
505
533
|
} else {
|
|
534
|
+
// Empty /new snapshot: any model that appeared is a selection (user or
|
|
535
|
+
// picker) — abort rather than overwrite it with the saved default.
|
|
536
|
+
// The baseline path below is only for startup/resume, where the first
|
|
537
|
+
// live model may be the race-recovered initial pick, not user intent.
|
|
538
|
+
if (reason === "new") {
|
|
539
|
+
log.debug(
|
|
540
|
+
"restore-default-model abort",
|
|
541
|
+
`empty /new session; live selection appeared ` +
|
|
542
|
+
`${live.provider}/${live.id} (${phase} attempt ${attempt}/${retries})`,
|
|
543
|
+
);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
506
546
|
// Empty snapshot: first non-default live model is baseline, not abort.
|
|
507
547
|
// Only abort when live later *changes* from that mid-flight selection.
|
|
508
548
|
if (!firstSeenLive) {
|
|
@@ -568,7 +608,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
|
|
|
568
608
|
"restore-default-model",
|
|
569
609
|
`restored ${label} ` +
|
|
570
610
|
`(was ${currentProvider ?? "none"}/${currentModelId ?? "none"}; ` +
|
|
571
|
-
`session_start reason=${
|
|
611
|
+
`session_start reason=${reason || "?"}; attempt ${attempt}/${retries})`,
|
|
572
612
|
);
|
|
573
613
|
return true;
|
|
574
614
|
}
|
|
@@ -590,7 +630,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
|
|
|
590
630
|
log.warn(
|
|
591
631
|
"restore-default-model exhausted",
|
|
592
632
|
`${lastFailReason} after ${retries} attempts ` +
|
|
593
|
-
`(session_start reason=${
|
|
633
|
+
`(session_start reason=${reason || "?"})`,
|
|
594
634
|
);
|
|
595
635
|
return false;
|
|
596
636
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Commit and push the current change set via git-expert — /create-commit <message>"
|
|
3
|
+
argument-hint: "<message>"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Raw Arguments
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
$ARGUMENTS
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
# Create Commit Prompt
|
|
13
|
+
|
|
14
|
+
Generates the git-expert commit delegation from the template instead of composing
|
|
15
|
+
the instruction prose. Only the file list and message vary per commit.
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
### Step 1: Read the Template
|
|
20
|
+
|
|
21
|
+
Read the template file:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
~/.pi/agent/npm/node_modules/pi-orchestrator-config/templates/commit-prompt.md
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Developer checkout fallback: `templates/commit-prompt.md` in the pi-config repo.
|
|
28
|
+
This is the immutable source template. NEVER modify it.
|
|
29
|
+
|
|
30
|
+
### Step 2: Fill Placeholders
|
|
31
|
+
|
|
32
|
+
| Placeholder | Value |
|
|
33
|
+
|-------------|-------|
|
|
34
|
+
| `{{FILES}}` | Every file to commit, one hyphen bullet per file, exact paths — parse `git status --porcelain=v1 -z` per the rules below |
|
|
35
|
+
| `{{MESSAGE}}` | Commit subject — the FIRST LINE of the raw arguments, nothing more. If empty, draft a conventional `type(scope): description` subject from the change set and confirm with the user |
|
|
36
|
+
| `{{BODY}}` | The remainder of the raw arguments after the first line, if any — never duplicates `{{MESSAGE}}` content. Empty by default |
|
|
37
|
+
|
|
38
|
+
**Parsing `git status --porcelain=v1 -z`** (NUL-delimited; avoids quote ambiguity):
|
|
39
|
+
|
|
40
|
+
- Rename (`R`) and copy (`C`) records: list only the NEW/destination path
|
|
41
|
+
- C-escaped quoted paths: decode to the real path; deleted paths: keep as-is
|
|
42
|
+
- Paths containing newlines or control characters: render them in `printf '%q'` shell-escaped form so the bullet keeps one line — git-expert decodes before staging
|
|
43
|
+
|
|
44
|
+
If `git status` shows files that clearly should not be committed (generated
|
|
45
|
+
artifacts, `.env`, secrets), exclude them from `{{FILES}}` and say so.
|
|
46
|
+
|
|
47
|
+
Never leave a `{{...}}` token in the output. If the commit text contains a run of
|
|
48
|
+
four or more backticks, lengthen the template's code fences to exceed it
|
|
49
|
+
(fences must always out-length any backtick run inside).
|
|
50
|
+
|
|
51
|
+
### Step 3: Delegate
|
|
52
|
+
|
|
53
|
+
Delegate the filled template to `git-expert` **verbatim** — no extra prose,
|
|
54
|
+
no re-wording, no added instructions.
|
|
55
|
+
|
|
56
|
+
### Step 4: Report
|
|
57
|
+
|
|
58
|
+
Report git-expert's result: commit sha, branch, push outcome.
|
|
@@ -371,11 +371,27 @@ No "this looks similar to the one I already fixed."**
|
|
|
371
371
|
|
|
372
372
|
If any approved tasks weren't implemented, review with user.
|
|
373
373
|
|
|
374
|
-
### Phase 5:
|
|
374
|
+
### Phase 5: Prepare Reply Mappings
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
🚨 **NEVER hand-edit the reviews JSON.** No python heredocs, no `jq`, no manual
|
|
377
|
+
`status`/`reply` field mutation. The `myk-pi-tools reviews address` command applies
|
|
378
|
+
replies, flips statuses, posts, and stores in one step — you only prepare mapping files.
|
|
377
379
|
|
|
378
|
-
|
|
380
|
+
Write one mapping file per target status. Each line targets one finding:
|
|
381
|
+
|
|
382
|
+
```text
|
|
383
|
+
<path>:<line>|<reply>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- `<path>:<line>` — the finding's fetched `path` and `line`. A thread finding and its
|
|
387
|
+
paired sticky finding are addressed together automatically (the command handles
|
|
388
|
+
dedupe and pairing — you never target stickies separately).
|
|
389
|
+
- `*` — matches ALL pending findings (single-line mapping file).
|
|
390
|
+
- Group findings by their target status — Phase 8 runs one `reviews address` call per
|
|
391
|
+
status group.
|
|
392
|
+
- Posting happens in Phase 8, AFTER commit/push — do not post before the fixes are pushed.
|
|
393
|
+
|
|
394
|
+
**Valid status values (the `--set` flag for each mapping group):**
|
|
379
395
|
|
|
380
396
|
| Status | Behavior |
|
|
381
397
|
|--------|----------|
|
|
@@ -438,44 +454,52 @@ Skip asking the user — commit and push automatically.
|
|
|
438
454
|
Code must be pushed before posting replies so that reviewers can see the fixes
|
|
439
455
|
when threads are resolved.
|
|
440
456
|
|
|
441
|
-
### Phase 8: Post Replies
|
|
457
|
+
### Phase 8: Post Replies (via reviews address)
|
|
442
458
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
**Body comments (outside-diff, major, minor, nitpick, duplicate, qodo sticky):**
|
|
446
|
-
|
|
447
|
-
Comments that don't have GitHub review threads (e.g., CodeRabbit outside-diff,
|
|
448
|
-
major, minor, nitpick, and duplicate comments) are replied to via a single
|
|
449
|
-
consolidated PR comment per reviewer. The comment mentions the reviewer (e.g., `@coderabbitai`)
|
|
450
|
-
and includes sections for each comment with the decision made. This ensures
|
|
451
|
-
automated reviewers know their comments were reviewed and won't re-raise them.
|
|
452
|
-
|
|
453
|
-
Post replies to GitHub:
|
|
459
|
+
Run one `reviews address` call per status group prepared in Phase 5 — each call applies
|
|
460
|
+
the mapping, posts replies, resolves threads, and stores to the database:
|
|
454
461
|
|
|
455
462
|
```bash
|
|
456
|
-
myk-pi-tools reviews
|
|
463
|
+
myk-pi-tools reviews address --json {json_path} --replies {mapping_file} --set addressed
|
|
464
|
+
myk-pi-tools reviews address --json {json_path} --replies {mapping_file} --set skipped
|
|
457
465
|
```
|
|
458
466
|
|
|
459
|
-
|
|
460
|
-
The command prints an ACTION REQUIRED message with the exact retry command.
|
|
461
|
-
Re-run it to retry — only unposted entries are retried. Repeat until all succeed.
|
|
462
|
-
|
|
463
|
-
**Output verification (MANDATORY):**
|
|
464
|
-
|
|
465
|
-
After `reviews post` completes successfully, check the output:
|
|
467
|
+
The command picks the newest `pr-*-reviews.json` in `.pi/tmp` when `--json` is omitted.
|
|
466
468
|
|
|
467
|
-
-
|
|
468
|
-
-
|
|
469
|
-
|
|
470
|
-
|
|
469
|
+
**Multi-group retention:** while any findings remain pending (unposted, not
|
|
470
|
+
auto-skipped), a successful run posts its group but does NOT store — the JSON is
|
|
471
|
+
kept on disk for the remaining status-group runs. The final run (nothing pending)
|
|
472
|
+
stores to the database and consumes the JSON. Run the groups in any order.
|
|
471
473
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
Store to database:
|
|
474
|
+
**Body comments (outside-diff, major, minor, nitpick, duplicate, qodo sticky):**
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
476
|
+
Comments that don't have GitHub review threads (e.g., CodeRabbit outside-diff,
|
|
477
|
+
major, minor, nitpick, and duplicate comments) are replied to via a single
|
|
478
|
+
consolidated PR comment per reviewer — `reviews address` handles this exactly like
|
|
479
|
+
`reviews post` did: the comment mentions the reviewer (e.g., `@coderabbitai`) and
|
|
480
|
+
includes sections for each comment with the decision made. This ensures automated
|
|
481
|
+
reviewers know their comments were reviewed and won't re-raise them.
|
|
482
|
+
|
|
483
|
+
**Failure semantics (MANDATORY):**
|
|
484
|
+
|
|
485
|
+
- **Exit 0** → all matched findings posted. Threads are resolved per the
|
|
486
|
+
Phase 5 status table — `addressed` (and AI-source `skipped`/`not_addressed`) threads
|
|
487
|
+
are resolved; **human `skipped`/`not_addressed` threads intentionally stay open** for
|
|
488
|
+
follow-up. Exit 0 never means every thread is resolved.
|
|
489
|
+
Storage happens only on the FINAL group: while pending findings remain the run posts
|
|
490
|
+
without storing (results live in the retained JSON until the last group's run stores
|
|
491
|
+
and consumes it).
|
|
492
|
+
- **Non-zero exit** → the JSON is RETAINED on disk for a corrected re-run.
|
|
493
|
+
Read the printed error:
|
|
494
|
+
- `no findings matched` → a mapping `path:line` matches no pending finding — fix the
|
|
495
|
+
mapping file and re-run. Already-posted entries are never touched again, so re-runs
|
|
496
|
+
are always safe.
|
|
497
|
+
- Post/resolve failures → the output names the entries that failed; re-run the same
|
|
498
|
+
command. Re-runs retry BOTH unposted entries AND posted-but-unresolved threads
|
|
499
|
+
(entries with `posted_at` but no resolution are resumed automatically — a failed
|
|
500
|
+
resolve retries the already-posted thread).
|
|
501
|
+
- **NEVER delete or hand-edit a retained JSON** — correct the mapping file and re-run
|
|
502
|
+
`reviews address`.
|
|
479
503
|
|
|
480
504
|
### Phase 9: Auto Polling Loop (--autorabbit / --autoqodo mode)
|
|
481
505
|
|
|
@@ -48,7 +48,7 @@ mentally or separately.
|
|
|
48
48
|
|
|
49
49
|
The review loop repeats for at most `review_loop_max_cycles` cycles (default `3`, range `1`-`10` only;
|
|
50
50
|
current value: `{{REVIEW_LOOP_MAX_CYCLES}}`), including the first cycle. Invalid values fall through to the
|
|
51
|
-
next resolution layer / default `3` — see `
|
|
51
|
+
next resolution layer / default `3` — see `docs/project-settings.md` for the full resolution order.
|
|
52
52
|
Disable the review loop via `review_loop_enforcement: false` — not via max_cycles.
|
|
53
53
|
|
|
54
54
|
Cap check is after 5a; it only blocks `go to 2` (re-dispatch of step 2 / all 6 agents,
|
|
@@ -5,8 +5,8 @@ After ANY code is added, changed, or removed, check if documentation needs updat
|
|
|
5
5
|
| Change | Check these files |
|
|
6
6
|
|--------|-------------------|
|
|
7
7
|
| New feature/command/tool | `README.md` (feature table, usage examples) |
|
|
8
|
-
| New or modified extension module | `
|
|
9
|
-
| New agent added/removed | `
|
|
8
|
+
| New or modified extension module | `contributing/repo-structure.md` (repository structure) |
|
|
9
|
+
| New agent added/removed | `contributing/repo-structure.md` (structure), `rules/10-agent-routing.md` (routing table), `rules/50-agent-bug-reporting.md` (agent list) |
|
|
10
10
|
| New prompt template | `README.md` (prompt templates table) |
|
|
11
11
|
| Docker/container changes | `README.md` (Docker section, shell alias, mounts), `Dockerfile` |
|
|
12
12
|
| New CLI tool or dependency | `README.md` (tools table), `Dockerfile` |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Commit Delegation — Template
|
|
2
|
+
|
|
3
|
+
> **Usage:** The orchestrator fills every `{{PLACEHOLDER}}` and delegates the
|
|
4
|
+
> filled text to git-expert. Delete this usage block when done.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Commit and push the current change set.
|
|
9
|
+
|
|
10
|
+
**Files to stage — EXACTLY these, never `git add .` or `git add -A`:**
|
|
11
|
+
|
|
12
|
+
{{FILES}}
|
|
13
|
+
|
|
14
|
+
**Commit message** (subject only — never body content):
|
|
15
|
+
|
|
16
|
+
````text
|
|
17
|
+
{{MESSAGE}}
|
|
18
|
+
````
|
|
19
|
+
|
|
20
|
+
**Commit body** (omit entirely if empty):
|
|
21
|
+
|
|
22
|
+
````text
|
|
23
|
+
{{BODY}}
|
|
24
|
+
````
|
|
25
|
+
|
|
26
|
+
**Steps:**
|
|
27
|
+
|
|
28
|
+
1. Stage each file listed above individually — always `git add -- <file>`
|
|
29
|
+
(the `--` terminator is mandatory; never let a path be read as an option),
|
|
30
|
+
one command naming every file is fine. Quote paths containing spaces or
|
|
31
|
+
special characters; if a path contains glob characters or pathspec magic
|
|
32
|
+
(`*`, `?`, `:` …), stage it as `git add -- ':(literal)<file>'` so it is
|
|
33
|
+
treated as an exact path, never a pattern. If a path was rendered in
|
|
34
|
+
shell-escaped form (`printf '%q'`), decode it before staging.
|
|
35
|
+
2. Commit with the message (and body if present) above — use a HEREDOC for multi-line.
|
|
36
|
+
The message and body must never duplicate each other.
|
|
37
|
+
3. Push the current branch to origin (set upstream with `-u` if the branch has none).
|
|
38
|
+
4. Report: commit sha, branch, push result.
|