ghostrail 0.7.5 → 0.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/dist/agent/result.d.ts +2 -1
- package/dist/agent/result.d.ts.map +1 -1
- package/dist/agent/result.js +7 -1
- package/dist/agent/result.js.map +1 -1
- package/dist/cli/commands.d.ts +26 -3
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +426 -7
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +39 -1
- package/dist/commands/help.js.map +1 -1
- package/dist/global/index.d.ts +1 -0
- package/dist/global/index.d.ts.map +1 -1
- package/dist/global/index.js +1 -0
- package/dist/global/index.js.map +1 -1
- package/dist/global/registry.d.ts +63 -0
- package/dist/global/registry.d.ts.map +1 -0
- package/dist/global/registry.js +120 -0
- package/dist/global/registry.js.map +1 -0
- package/dist/global/store.d.ts +10 -0
- package/dist/global/store.d.ts.map +1 -1
- package/dist/global/store.js +25 -1
- package/dist/global/store.js.map +1 -1
- package/dist/init/drift.d.ts +68 -0
- package/dist/init/drift.d.ts.map +1 -0
- package/dist/init/drift.js +103 -0
- package/dist/init/drift.js.map +1 -0
- package/dist/loop/engine.d.ts.map +1 -1
- package/dist/loop/engine.js +11 -4
- package/dist/loop/engine.js.map +1 -1
- package/dist/loop/ports.d.ts +19 -1
- package/dist/loop/ports.d.ts.map +1 -1
- package/dist/publish/githost.d.ts +9 -3
- package/dist/publish/githost.d.ts.map +1 -1
- package/dist/publish/githost.js +18 -4
- package/dist/publish/githost.js.map +1 -1
- package/dist/publish/github-publisher.d.ts +2 -2
- package/dist/publish/github-publisher.d.ts.map +1 -1
- package/dist/publish/github-publisher.js +3 -3
- package/dist/publish/github-publisher.js.map +1 -1
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +3 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/manager.d.ts +69 -0
- package/dist/service/manager.d.ts.map +1 -0
- package/dist/service/manager.js +107 -0
- package/dist/service/manager.js.map +1 -0
- package/dist/service/unit.d.ts +60 -0
- package/dist/service/unit.d.ts.map +1 -0
- package/dist/service/unit.js +111 -0
- package/dist/service/unit.js.map +1 -0
- package/package.json +1 -1
- package/skill/ghostrail/SKILL.md +62 -16
- package/templates/code-local/prompts/resolve-issue.md +15 -1
- package/templates/content-loop/prompts/draft.md +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { spawnCollect } from "../backend/proc.js";
|
|
6
|
+
import { parseUnitStates, renderSystemdUnit, unitName } from "./unit.js";
|
|
7
|
+
/** Which manager this platform offers, whether or not it is supported. */
|
|
8
|
+
export async function detectManager() {
|
|
9
|
+
if (process.platform === "darwin") {
|
|
10
|
+
const launchctl = await spawnCollect(["launchctl", "help"], { timeoutMs: 5_000 });
|
|
11
|
+
// launchctl exits nonzero for `help`, so presence is what is being tested.
|
|
12
|
+
return launchctl.exitCode === 127 ? "none" : "launchd";
|
|
13
|
+
}
|
|
14
|
+
const systemctl = await spawnCollect(["systemctl", "--user", "--version"], {
|
|
15
|
+
timeoutMs: 5_000,
|
|
16
|
+
});
|
|
17
|
+
return systemctl.exitCode === 0 ? "systemd" : "none";
|
|
18
|
+
}
|
|
19
|
+
/** Where systemd looks for this user's units. */
|
|
20
|
+
export function systemdUnitDir(env = process.env) {
|
|
21
|
+
const base = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
22
|
+
return join(base, "systemd", "user");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Write a unit unless it is already right, or has been edited.
|
|
26
|
+
*
|
|
27
|
+
* An edited unit is never overwritten. Same instinct as `init --update`: a
|
|
28
|
+
* customization is deliberate until proven otherwise, and silently reverting
|
|
29
|
+
* one is the kind of thing that makes a tool untrustworthy.
|
|
30
|
+
*/
|
|
31
|
+
export async function installUnit(spec, env = process.env) {
|
|
32
|
+
const unit = unitName(spec.name, spec.path);
|
|
33
|
+
const dir = systemdUnitDir(env);
|
|
34
|
+
const file = join(dir, unit);
|
|
35
|
+
const wanted = renderSystemdUnit(spec);
|
|
36
|
+
let current;
|
|
37
|
+
try {
|
|
38
|
+
current = await readFile(file, "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
current = undefined;
|
|
42
|
+
}
|
|
43
|
+
if (current === wanted)
|
|
44
|
+
return { outcome: "unchanged", unit, file };
|
|
45
|
+
if (current !== undefined)
|
|
46
|
+
return { outcome: "hand-edited", unit, file };
|
|
47
|
+
await mkdir(dir, { recursive: true });
|
|
48
|
+
await writeFile(file, wanted, "utf8");
|
|
49
|
+
return { outcome: "written", unit, file };
|
|
50
|
+
}
|
|
51
|
+
/** Run a systemctl user subcommand, returning its exit code and output. */
|
|
52
|
+
async function systemctl(args) {
|
|
53
|
+
const result = await spawnCollect(["systemctl", "--user", ...args], { timeoutMs: 30_000 });
|
|
54
|
+
return { code: result.exitCode, out: `${result.stdout}${result.stderr}` };
|
|
55
|
+
}
|
|
56
|
+
/** Reload the manager so a newly written unit is visible. */
|
|
57
|
+
export async function reload() {
|
|
58
|
+
await systemctl(["daemon-reload"]);
|
|
59
|
+
}
|
|
60
|
+
/** Enable and start a unit. Returns an error message, or undefined on success. */
|
|
61
|
+
export async function enableAndStart(unit) {
|
|
62
|
+
const result = await systemctl(["enable", "--now", unit]);
|
|
63
|
+
return result.code === 0 ? undefined : result.out.trim();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stop and disable a unit.
|
|
67
|
+
*
|
|
68
|
+
* This can genuinely take minutes: an in-flight tick is not interrupted, so the
|
|
69
|
+
* manager waits out the current agent run. Callers should say so before this
|
|
70
|
+
* starts rather than letting it look hung.
|
|
71
|
+
*/
|
|
72
|
+
export async function disableAndStop(unit) {
|
|
73
|
+
const result = await systemctl(["disable", "--now", unit]);
|
|
74
|
+
if (result.code === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
// A unit that was never installed is not a failure to stop. The goal is that
|
|
77
|
+
// it is not running, and it is not. Reporting this as an error made `stop` on
|
|
78
|
+
// a never-started ghostrail look broken.
|
|
79
|
+
if (/does not exist|not loaded|no such file/i.test(result.out))
|
|
80
|
+
return undefined;
|
|
81
|
+
return result.out.trim();
|
|
82
|
+
}
|
|
83
|
+
/** Remove a unit file. Absent is success: the goal is that it is not there. */
|
|
84
|
+
export async function removeUnit(unit, env = process.env) {
|
|
85
|
+
await rm(join(systemdUnitDir(env), unit), { force: true });
|
|
86
|
+
}
|
|
87
|
+
/** Active state per unit, for the units asked about. */
|
|
88
|
+
export async function unitStates(units) {
|
|
89
|
+
if (units.length === 0)
|
|
90
|
+
return new Map();
|
|
91
|
+
const result = await systemctl(["show", "--property=Id", "--property=ActiveState", ...units]);
|
|
92
|
+
return parseUnitStates(result.out);
|
|
93
|
+
}
|
|
94
|
+
/** The real one: systemd through `systemctl --user`. */
|
|
95
|
+
export function systemdManager(env = process.env) {
|
|
96
|
+
return {
|
|
97
|
+
kind: detectManager,
|
|
98
|
+
install: (spec) => installUnit(spec, env),
|
|
99
|
+
enable: async (unit) => {
|
|
100
|
+
await reload();
|
|
101
|
+
return enableAndStart(unit);
|
|
102
|
+
},
|
|
103
|
+
disable: disableAndStop,
|
|
104
|
+
states: unitStates,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/service/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering and naming for the user service units that keep a ghostrail
|
|
3
|
+
* running. Pure: nothing here touches the disk or shells out, so the awkward
|
|
4
|
+
* parts (unit naming, the stop timeout) are testable without a service manager.
|
|
5
|
+
*/
|
|
6
|
+
/** Everything a unit file needs. */
|
|
7
|
+
export interface UnitSpec {
|
|
8
|
+
/** The ghostrail's display name, for the unit description. */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Absolute path to the repo. Becomes the working directory. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Absolute path to the ghostrail binary. */
|
|
13
|
+
exec: string;
|
|
14
|
+
/** Poll interval, as a duration string like `5m`. */
|
|
15
|
+
interval: string;
|
|
16
|
+
/** How long the manager waits for a clean stop, in seconds. */
|
|
17
|
+
stopTimeoutSec: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The unit file name for a ghostrail: `ghostrail-<slug>-<hash>.service`.
|
|
21
|
+
*
|
|
22
|
+
* The hash of the path is not decoration. `name` is display-only and duplicates
|
|
23
|
+
* are entirely legal: two repos scaffolded from the same template are both
|
|
24
|
+
* called `code-local`. Keying the unit on the name alone would give them one
|
|
25
|
+
* unit between them, so starting the second would silently hijack the first.
|
|
26
|
+
* The identity is the path, so the path is what disambiguates.
|
|
27
|
+
*/
|
|
28
|
+
export declare function unitName(name: string, path: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* How long the service manager must wait for a clean stop.
|
|
31
|
+
*
|
|
32
|
+
* `watchLoop` checks its abort signal *before* each tick and each sleep, so a
|
|
33
|
+
* SIGTERM arriving mid-tick does not interrupt the running agent: the tick
|
|
34
|
+
* finishes first. A manager that gives up sooner SIGKILLs a live agent run,
|
|
35
|
+
* orphaning its container and leaving the tracker issue claimed by a bot that no
|
|
36
|
+
* longer exists.
|
|
37
|
+
*
|
|
38
|
+
* The headroom is there because the agent timeout bounds the agent, not the
|
|
39
|
+
* tick: provisioning, the gate, and publishing all happen outside it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function stopTimeoutSeconds(timeoutMs: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* A systemd user unit. Deterministic, so an unchanged unit can be recognized
|
|
44
|
+
* and an edited one left alone.
|
|
45
|
+
*
|
|
46
|
+
* Paths are written as-is: a unit file is not shell, and a path containing a
|
|
47
|
+
* space is legal in `WorkingDirectory=`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderSystemdUnit(spec: UnitSpec): string;
|
|
50
|
+
/**
|
|
51
|
+
* Parse `systemctl --user show --property=Id --property=ActiveState` for
|
|
52
|
+
* several units into unit name -> active state.
|
|
53
|
+
*
|
|
54
|
+
* Tolerates what the real command emits: records separated by blank lines,
|
|
55
|
+
* properties in any order, extra properties, and a unit that does not exist
|
|
56
|
+
* (reported as `inactive` rather than as a failure). A record with no `Id` is
|
|
57
|
+
* skipped rather than fatal.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseUnitStates(stdout: string): Map<string, string>;
|
|
60
|
+
//# sourceMappingURL=unit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit.d.ts","sourceRoot":"","sources":["../../src/service/unit.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;CACxB;AAWD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3D;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAoBxD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBnE"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Lowercase, non-alphanumeric runs collapsed to `-`, trimmed. */
|
|
3
|
+
function slug(name) {
|
|
4
|
+
const out = name
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
7
|
+
.replace(/^-+|-+$/g, "");
|
|
8
|
+
return out.length > 0 ? out : "ghostrail";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The unit file name for a ghostrail: `ghostrail-<slug>-<hash>.service`.
|
|
12
|
+
*
|
|
13
|
+
* The hash of the path is not decoration. `name` is display-only and duplicates
|
|
14
|
+
* are entirely legal: two repos scaffolded from the same template are both
|
|
15
|
+
* called `code-local`. Keying the unit on the name alone would give them one
|
|
16
|
+
* unit between them, so starting the second would silently hijack the first.
|
|
17
|
+
* The identity is the path, so the path is what disambiguates.
|
|
18
|
+
*/
|
|
19
|
+
export function unitName(name, path) {
|
|
20
|
+
const hash = createHash("sha256").update(path, "utf8").digest("hex").slice(0, 8);
|
|
21
|
+
return `ghostrail-${slug(name)}-${hash}.service`;
|
|
22
|
+
}
|
|
23
|
+
/** Headroom over the agent timeout, in seconds. */
|
|
24
|
+
const STOP_HEADROOM_SEC = 300;
|
|
25
|
+
/** Never wait less than this for a stop, however short the configured timeout. */
|
|
26
|
+
const MIN_STOP_SEC = 600;
|
|
27
|
+
/**
|
|
28
|
+
* How long the service manager must wait for a clean stop.
|
|
29
|
+
*
|
|
30
|
+
* `watchLoop` checks its abort signal *before* each tick and each sleep, so a
|
|
31
|
+
* SIGTERM arriving mid-tick does not interrupt the running agent: the tick
|
|
32
|
+
* finishes first. A manager that gives up sooner SIGKILLs a live agent run,
|
|
33
|
+
* orphaning its container and leaving the tracker issue claimed by a bot that no
|
|
34
|
+
* longer exists.
|
|
35
|
+
*
|
|
36
|
+
* The headroom is there because the agent timeout bounds the agent, not the
|
|
37
|
+
* tick: provisioning, the gate, and publishing all happen outside it.
|
|
38
|
+
*/
|
|
39
|
+
export function stopTimeoutSeconds(timeoutMs) {
|
|
40
|
+
// Total by construction. The caller reads this from a validated config, so a
|
|
41
|
+
// negative or non-finite value should be unreachable, but this number is the
|
|
42
|
+
// one thing standing between a stop and a SIGKILL through a live agent run.
|
|
43
|
+
// Rendering `TimeoutStopSec=NaN` would make systemd reject the whole unit.
|
|
44
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
45
|
+
return MIN_STOP_SEC;
|
|
46
|
+
const seconds = Math.ceil(timeoutMs / 1000) + STOP_HEADROOM_SEC;
|
|
47
|
+
return Math.max(seconds, MIN_STOP_SEC);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A systemd user unit. Deterministic, so an unchanged unit can be recognized
|
|
51
|
+
* and an edited one left alone.
|
|
52
|
+
*
|
|
53
|
+
* Paths are written as-is: a unit file is not shell, and a path containing a
|
|
54
|
+
* space is legal in `WorkingDirectory=`.
|
|
55
|
+
*/
|
|
56
|
+
export function renderSystemdUnit(spec) {
|
|
57
|
+
return `${[
|
|
58
|
+
"[Unit]",
|
|
59
|
+
`Description=ghostrail factory (${spec.name})`,
|
|
60
|
+
"Documentation=https://github.com/timothyjordan/ghostrail",
|
|
61
|
+
"",
|
|
62
|
+
"[Service]",
|
|
63
|
+
"Type=simple",
|
|
64
|
+
`WorkingDirectory=${spec.path}`,
|
|
65
|
+
// Absolute: a bare `ghostrail` is not on a service manager's PATH, and on
|
|
66
|
+
// an nvm install the real path is version-specific.
|
|
67
|
+
`ExecStart=${spec.exec} watch --interval ${spec.interval}`,
|
|
68
|
+
"Restart=on-failure",
|
|
69
|
+
"RestartSec=30",
|
|
70
|
+
// Long enough for an in-flight tick to finish. See stopTimeoutSeconds.
|
|
71
|
+
`TimeoutStopSec=${spec.stopTimeoutSec}`,
|
|
72
|
+
"",
|
|
73
|
+
"[Install]",
|
|
74
|
+
"WantedBy=default.target",
|
|
75
|
+
].join("\n")}\n`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse `systemctl --user show --property=Id --property=ActiveState` for
|
|
79
|
+
* several units into unit name -> active state.
|
|
80
|
+
*
|
|
81
|
+
* Tolerates what the real command emits: records separated by blank lines,
|
|
82
|
+
* properties in any order, extra properties, and a unit that does not exist
|
|
83
|
+
* (reported as `inactive` rather than as a failure). A record with no `Id` is
|
|
84
|
+
* skipped rather than fatal.
|
|
85
|
+
*/
|
|
86
|
+
export function parseUnitStates(stdout) {
|
|
87
|
+
const states = new Map();
|
|
88
|
+
for (const record of stdout.split(/\n\s*\n/)) {
|
|
89
|
+
let id;
|
|
90
|
+
let active;
|
|
91
|
+
for (const line of record.split("\n")) {
|
|
92
|
+
const eq = line.indexOf("=");
|
|
93
|
+
if (eq === -1)
|
|
94
|
+
continue;
|
|
95
|
+
const key = line.slice(0, eq).trim();
|
|
96
|
+
const value = line.slice(eq + 1).trim();
|
|
97
|
+
if (key === "Id")
|
|
98
|
+
id = value;
|
|
99
|
+
else if (key === "ActiveState")
|
|
100
|
+
active = value;
|
|
101
|
+
}
|
|
102
|
+
// A record carrying an Id but no ActiveState becomes "unknown" rather than
|
|
103
|
+
// being dropped: the unit exists, and saying so beats pretending it does
|
|
104
|
+
// not. A repeated Id keeps the last record, matching how systemd's own
|
|
105
|
+
// later output would supersede earlier output.
|
|
106
|
+
if (id !== undefined && id.length > 0)
|
|
107
|
+
states.set(id, active ?? "unknown");
|
|
108
|
+
}
|
|
109
|
+
return states;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=unit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit.js","sourceRoot":"","sources":["../../src/service/unit.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/skill/ghostrail/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ghostrail
|
|
3
|
-
description: Drive ghostrail (the software factory) from inside this repo. Use when the user types /ghostrail, or asks to set up, customize, check, or run a ghostrail factory — scaffolding ghostrail.toml and prompts, tailoring the scaffolded prompts and gate commands to this codebase, diagnosing a broken setup, or kicking off a run/respond/triage tick.
|
|
3
|
+
description: Drive ghostrail (the software factory) from inside this repo. Use when the user types /ghostrail, or asks to set up, customize, check, or run a ghostrail factory — scaffolding ghostrail.toml and prompts, tailoring the scaffolded prompts and gate commands to this codebase, carrying a later template improvement into prompts you have customized, diagnosing a broken setup, or kicking off a run/respond/triage tick.
|
|
4
4
|
user-invocable: true
|
|
5
|
-
argument-hint: "[customize | init | doctor | status | run | respond | triage]"
|
|
5
|
+
argument-hint: "[customize | init | update | doctor | status | run | respond | triage]"
|
|
6
6
|
license: Apache-2.0
|
|
7
7
|
metadata:
|
|
8
|
-
version: 0.
|
|
8
|
+
version: 0.3.0
|
|
9
9
|
allowed-tools:
|
|
10
10
|
- Read
|
|
11
11
|
- Write
|
|
@@ -87,25 +87,71 @@ Scaffold the factory into this repo, then hand off to **customize**.
|
|
|
87
87
|
|
|
88
88
|
### Already initialized? Check for template drift
|
|
89
89
|
|
|
90
|
-
If the repo already has `ghostrail.toml` and
|
|
91
|
-
`init` and report "0 files written" as if nothing were wrong.
|
|
90
|
+
If the repo already has `ghostrail.toml` and prompts, do not re-run a bare
|
|
91
|
+
`init` and report "0 files written" as if nothing were wrong. See **update**
|
|
92
|
+
below.
|
|
93
|
+
|
|
94
|
+
Never suggest `--force` as the way to take a template update: it overwrites
|
|
95
|
+
every file, destroying the gate commands, tracker filters, and prompt edits that
|
|
96
|
+
**customize** wrote.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## update
|
|
101
|
+
|
|
102
|
+
Carry a template improvement into a repo that has customized its prompts. Run:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
<GR> init <template> --update
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
It writes only files this repo never edited (proved by the baseline in
|
|
109
|
+
`ghostrail.template.json`), and it never touches a customized file. For those it
|
|
110
|
+
prints **signals**: what the template gained that the repo's copy lacks.
|
|
92
111
|
|
|
93
112
|
```
|
|
94
|
-
|
|
113
|
+
ghostrail/prompts/resolve-issue.md
|
|
114
|
+
customized, and the template gained:
|
|
115
|
+
placeholder {{description}}
|
|
116
|
+
result field "testPlan"
|
|
95
117
|
```
|
|
96
118
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
Placing those is your job, and it is not a text merge. A customized prompt can
|
|
120
|
+
be a near-total rewrite that still needs the same one-line addition, so port the
|
|
121
|
+
**intent**, not the template's wording:
|
|
122
|
+
|
|
123
|
+
1. Read the template's copy of the file to see how it uses the signal. The path
|
|
124
|
+
is printed by `<GR> init <template> --diff`.
|
|
125
|
+
2. Read the repo's copy and find where the same idea belongs *in its structure*.
|
|
126
|
+
3. Add it in the repo's own voice. A placeholder goes where that content is
|
|
127
|
+
wanted (`{{description}}` under the issue's ID/title/link block). A result
|
|
128
|
+
field goes on the matching line of the result-document list, with a
|
|
129
|
+
description written to match the surrounding entries.
|
|
130
|
+
4. Never reformat, reorder, or "tidy" the rest of the file. The customization is
|
|
131
|
+
deliberate. Your diff should be as small as the signal count implies.
|
|
132
|
+
5. Re-run `<GR> init <template> --update` and confirm it now reports
|
|
133
|
+
`customized, nothing to take`.
|
|
134
|
+
|
|
135
|
+
Verify a placeholder actually renders before anyone spends a run on it:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
node -e 'import("URL_TO_PROMPT_JS").then(({renderPrompt})=>{
|
|
139
|
+
const t=require("fs").readFileSync("PATH/TO/PROMPT.md","utf8");
|
|
140
|
+
const o=renderPrompt(t,{id:"TJ-1",title:"t",url:"u",description:"BODY"});
|
|
141
|
+
console.log(o.match(/\{\{[a-z]+\}\}/g) ?? "no unrendered placeholders");
|
|
142
|
+
})'
|
|
143
|
+
```
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
file, destroying the gate commands, tracker filters, and prompt edits that
|
|
104
|
-
**customize** wrote. Merging by hand is the correct path.
|
|
145
|
+
Two things to tell the user plainly:
|
|
105
146
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
- **The installed template is the last published release.** A repo can be
|
|
148
|
+
current with it and still behind `main`. The `--update` header names the
|
|
149
|
+
version it compared against; repeat that in your summary.
|
|
150
|
+
- **A repo with no baseline gets signals but no writes.** Without a recorded
|
|
151
|
+
baseline nothing can be *shown* to be untouched, so `--update` will not
|
|
152
|
+
overwrite anything. Re-running a bare `<GR> init` once adopts a baseline for
|
|
153
|
+
every file that still matches the template exactly, which is safe and makes
|
|
154
|
+
later updates able to write.
|
|
109
155
|
|
|
110
156
|
---
|
|
111
157
|
|
|
@@ -26,10 +26,24 @@ Match the surrounding code: its style, patterns, and test conventions.
|
|
|
26
26
|
## Reporting your result (required, do this last)
|
|
27
27
|
Write a JSON file at `.ghostrail/result.json` with exactly one of:
|
|
28
28
|
|
|
29
|
-
- `{"status":"done","summary":"<what
|
|
29
|
+
- `{"status":"done","summary":"<what changed and why>","testPlan":"<how to check it by hand>","type":"<feat|fix|docs|refactor|chore|...>"}`
|
|
30
30
|
- `{"status":"blocked","questions":"<a real product/design decision you need>"}`
|
|
31
31
|
- `{"status":"failed","error":"<why>"}`
|
|
32
32
|
- `{"status":"noop","reason":"<why nothing needed changing>"}`
|
|
33
33
|
|
|
34
34
|
`type` is the conventional-commit type for the change; it sets the commit and PR
|
|
35
35
|
title. If you omit it, the factory's configured default is used.
|
|
36
|
+
|
|
37
|
+
`summary` and `testPlan` are the pull request a human reads, so write them for
|
|
38
|
+
that reader rather than as a log of what you did:
|
|
39
|
+
|
|
40
|
+
- **`summary`**: one or two sentences on what changed and why, then markdown
|
|
41
|
+
bullets for the specifics. Do not write a single long paragraph, and do not
|
|
42
|
+
add headings; the factory supplies its own.
|
|
43
|
+
- **`testPlan`**: the steps someone runs to check this by hand. Concrete
|
|
44
|
+
commands, what to look at, and what they should see. The gate has already run
|
|
45
|
+
lint, typecheck, and the unit suite, so do not just repeat those: give the
|
|
46
|
+
commands that exercise what you actually changed. If the change is not
|
|
47
|
+
observable by hand (a refactor, a type fix), say what to run instead and what
|
|
48
|
+
a green result proves.
|
|
49
|
+
|
|
@@ -25,7 +25,7 @@ strategy). Follow them closely: they define how this work should read.
|
|
|
25
25
|
## Reporting your result (required, do this last)
|
|
26
26
|
Write `.ghostrail/result.json` with exactly one of:
|
|
27
27
|
|
|
28
|
-
- `{"status":"done","summary":"<
|
|
28
|
+
- `{"status":"done","summary":"<what you drafted and why>","testPlan":"<how to review it>"}`
|
|
29
29
|
- `{"status":"blocked","questions":"<what you need decided>"}`
|
|
30
30
|
- `{"status":"failed","error":"<why>"}`
|
|
31
31
|
- `{"status":"noop","reason":"<why nothing needed changing>"}`
|