jorgex-stack 1.9.14 → 1.9.16
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 +5 -27
- package/package.json +1 -1
- package/stack/skills/orchestrator/SKILL.md +6 -0
- package/stack/skills/orchestrator/references/standard-workflow.md +3 -3
- package/stack/skills/work-lifecycle/SKILL.md +4 -4
- package/stack/skills/work-lifecycle/references/plan-template.md +6 -5
- package/stack/skills/work-lifecycle/references/pr-continuation.md +27 -0
- package/stack/system-prompt/AGENTS.md +1 -1
- package/stack/commands/opencode/goal.md +0 -7
- package/stack/plugins/opencode/goal/artifacts.ts +0 -142
- package/stack/plugins/opencode/goal/command.ts +0 -255
- package/stack/plugins/opencode/goal/db.ts +0 -68
- package/stack/plugins/opencode/goal/opencode-hooks.ts +0 -311
- package/stack/plugins/opencode/goal/state.ts +0 -85
- package/stack/plugins/opencode/goal/store.ts +0 -906
- package/stack/plugins/opencode/goal/supervisor.ts +0 -269
- package/stack/plugins/opencode/goal/types.ts +0 -187
- package/stack/plugins/opencode/goal-plugin.ts +0 -184
package/README.md
CHANGED
|
@@ -185,35 +185,13 @@ Usage:
|
|
|
185
185
|
|
|
186
186
|
GitHub authentication: requests use `GH_TOKEN`/`GITHUB_TOKEN` from the environment or, if unavailable, the token from your `gh` CLI session (`gh auth token` — local read only, never logged or persisted). Without a token, GitHub limits parallel requests and some upstreams may appear as "offline".
|
|
187
187
|
|
|
188
|
-
### OpenCode Goal Mode
|
|
188
|
+
### OpenCode Goal Mode retirado
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
Goal Mode de OpenCode se ha retirado. Stack ya no instala su plugin ni el comando `/goal`; la continuidad de trabajo usa el lifecycle normal: `work/{name}/PRD.md` y `plan.md` permanecen durante los merges intermedios, `work/{name}/pr/{NN}` conserva cada checkpoint y `work/{name}/done` queda reservado para el cierre final.
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
La retirada no migra el historial. `sync` sólo puede retirar archivos gestionados cuando dispone de un manifest legible que registre esas rutas como gestionadas y un inventario completo, con backup previo; conserva los datos existentes en `~/.jorgex-stack/goals` y los plugins ajenos cuando el manifest del Stack está íntegro. El registro local no autentica la propiedad ni corrige una lista `owned` editada o inconsistente: si falta, no se puede parsear o el inventario es incompleto, no se borra el legacy. Ante sospecha sobre el manifest, no ejecutes `sync`; revísalo o restáuralo con backup. `--target-dir` no implica limpiar ese estado.
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
- `/goal <goal>` — creates a persistent goal.
|
|
197
|
-
- `/goal status` — shows status and next action.
|
|
198
|
-
- `/goal plan` — shows the goal's master plan / PRD.
|
|
199
|
-
- `/goal history` — lists events and transitions.
|
|
200
|
-
- `/goal pause` — pauses the goal.
|
|
201
|
-
- `/goal resume` — resumes the goal.
|
|
202
|
-
- `/goal merged [commit]` — signals that the pending external PR has been merged.
|
|
203
|
-
- `/goal cancel` — cancels the goal.
|
|
204
|
-
|
|
205
|
-
What does not exist:
|
|
206
|
-
|
|
207
|
-
- `/goal quick`
|
|
208
|
-
- `/goal work`
|
|
209
|
-
|
|
210
|
-
Operational state:
|
|
211
|
-
|
|
212
|
-
- Separate SQLite database by default at `~/.jorgex-stack/goals/goals.sqlite`.
|
|
213
|
-
- Optional override with `JORGEX_GOAL_DB`, but always inside `~/.jorgex-stack/goals/`.
|
|
214
|
-
- Engram is not the goal's operational store: it remains memory/protocol, not the state database.
|
|
215
|
-
- Goal Mode does not perform automatic merges; when it must wait for an external merge, the state becomes `waiting_for_merge`.
|
|
216
|
-
- The integration uses experimental OpenCode hooks (`experimental.chat.system.transform` and `experimental.session.compacting`), so that surface may change.
|
|
194
|
+
La continuidad entre checkpoints sigue requiriendo trabajo aprobado, capacidades disponibles y merge humano explícito. PiGoal conserva su propio lifecycle. No se afirma disponibilidad de una alternativa GoalV2 de OpenCode.
|
|
217
195
|
|
|
218
196
|
## Status
|
|
219
197
|
|
|
@@ -253,7 +231,7 @@ La integración de este App pertenece al release del repositorio Stack. No conce
|
|
|
253
231
|
|
|
254
232
|
## Development
|
|
255
233
|
|
|
256
|
-
Requirements: Node >= 22.5 and pnpm (never npm).
|
|
234
|
+
Requirements: Node >= 22.5 and pnpm (never npm).
|
|
257
235
|
|
|
258
236
|
```
|
|
259
237
|
pnpm install
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jorgex-stack",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.16",
|
|
4
4
|
"description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -90,6 +90,12 @@ Use the work already performed and its existing evidence/checkpoint; do not laun
|
|
|
90
90
|
|
|
91
91
|
Respect the active output contract. In programmatic mode, keep the strict final JSON and its existing keys/types: put changes and factual workflow feedback in `summary`, current limitations in `risks` and pending actions in `next_steps`, preserving PR metadata in allowed text fields. Do not add keys, a `ready` status value, Markdown fences or prose outside that final JSON. Intermediate progress uses the permitted channel rather than pretending to be another final response.
|
|
92
92
|
|
|
93
|
+
### Continue after a ready checkpoint
|
|
94
|
+
|
|
95
|
+
A verified ready PR is a checkpoint, not an automatic pause. Give the Ready handoff and continue approved remaining work that can be verified with the available capabilities and project rules. An unavailable publication, migration, deployment or material decision blocks its consumers, not unrelated approved work. Do not invent another task just to keep running, and never infer merge permission from plan approval.
|
|
96
|
+
|
|
97
|
+
For multi-PR work or a dependency on an unmerged PR, read [PR continuation](../work-lifecycle/references/pr-continuation.md) relative to this skill directory before choosing the next base; if unavailable, report that boundary rather than guessing a dependent base. Keep parents ready and immutable; only a permitted child branch/worktree may advance. Stop when no approved safe work remains or a real capability/decision block prevents it. A runtime supervisor that pauses for an open PR remains a real constraint; this policy does not override its state machine or create background execution.
|
|
98
|
+
|
|
93
99
|
## Closing rule
|
|
94
100
|
|
|
95
101
|
Do not declare work finished after analysis or planning alone: complete the routed execution or report the concrete blocker.
|
|
@@ -10,7 +10,7 @@ INIT → EXPLORE → SPEC → PLAN → EXECUTE → VERIFY → SHIP → CLOSE
|
|
|
10
10
|
|
|
11
11
|
### Autonomy
|
|
12
12
|
|
|
13
|
-
The human drives the flow UP TO the plan: the idea,
|
|
13
|
+
The human drives the flow UP TO the plan: the idea, PRD and plan review are interactive. Once approved, EXECUTE → VERIFY → SHIP run **autonomously** within that scope: commits, work-branch pushes, draft PR creation, review and ready transitions are authorized, not merges. Resolve task-critical uncertainty from available context first; ask the user only for a genuinely unresolved material decision. CLOSE finishes the current checkpoint and follows [Continue after a ready checkpoint](../SKILL.md#continue-after-a-ready-checkpoint), not a mandatory pause while safe approved work remains. Every merge still requires an explicit user order. Keep `work/{name}/PRD.md` and `plan.md` until the roadmap is finished; choose production or a permitted parent base through the common continuation rule, never through an assumed need to wait for every merge.
|
|
14
14
|
|
|
15
15
|
## 1. INIT
|
|
16
16
|
|
|
@@ -155,7 +155,7 @@ An early review during EXECUTE is an **exception**, not a default phase. Use it
|
|
|
155
155
|
|
|
156
156
|
## 7. SHIP (automatic)
|
|
157
157
|
|
|
158
|
-
When the
|
|
158
|
+
When the current checkpoint's planned work is applied and VERIFY passes:
|
|
159
159
|
|
|
160
160
|
1. Confirm the draft PR exists, the worktree is clean, and the draft head matches the local HEAD. Complete any necessary documentation under the common rule and inspect the consolidated final diff against the PR's real base; do not publish intermediate behavior with required documentation missing.
|
|
161
161
|
2. Apply **Final review and PR lifecycle** in the entry [SKILL.md](../SKILL.md) and the project's review requirements. Reuse valid prior review evidence; choosing standard does not require another panel. Process the review findings by their three levels:
|
|
@@ -170,7 +170,7 @@ When the plan is fully applied and VERIFY passes:
|
|
|
170
170
|
|
|
171
171
|
## 8. CLOSE
|
|
172
172
|
|
|
173
|
-
-
|
|
173
|
+
- After configured Quality Gates pass for the current candidate, or after confirming that none are configured, close this checkpoint with the common [Ready handoff](../SKILL.md#ready-handoff). Preserve metadata, summarize changes and observed feedback, and report valid findings applied vs deferred and whether manual testing is advisable. Then follow [Continue after a ready checkpoint](../SKILL.md#continue-after-a-ready-checkpoint): continue approved safe work, or explain the real blocker/end of scope. Do not claim merge, deployment or overall roadmap completion from ready.
|
|
174
174
|
- NEVER merge the PR yourself — merge only on an explicit user order. After each intermediate merge: persist the checkpoint to `work/{name}/pr/{NN}`, update `plan.md`, and keep `work/{name}/` alive. After the final merge: persist the final outcome to memory, clean up `work/{name}/` and remove the worktree (see Work state).
|
|
175
175
|
- If the repo has its own skill for the closing steps (release, deploy, git, cleanup), that skill takes precedence over the default behavior.
|
|
176
176
|
|
|
@@ -46,11 +46,11 @@ Use this section only after routing selects formal SDD work, including work prom
|
|
|
46
46
|
- Only a formal task with an assigned phase outcome saves it BEFORE its final report; its outcome topic_key must be distinct from the `Spec` reference. Never use `mem_save` or `mem_update` to write a result over the Spec observation or its topic_key, or overwrite a Markdown Spec with an outcome. If no separate outcome destination was assigned, return the result to the coordinator for routing. An inline microassignment returns its evidence to the parent and has no separate spec or phase outcome. This does not waive mandatory immediate saves for decisions or findings.
|
|
47
47
|
- Task status lives ONLY in the task table: flip it (⬜ → ✅) with a surgical edit when the task closes. PR status/evidence lives ONLY in the PR roadmap table. Do not mirror task progress into memory, and do not re-read the whole plan after every task — it is already in context; re-read it on resume.
|
|
48
48
|
- Success criteria live ONLY in the plan's `SC-*` list. Task-to-criterion coverage lives ONLY in the task table's `SC` column. Verification/merge evidence lives ONLY in the PR roadmap or checkpoint that observed it; cite the relevant SC IDs there instead of copying the criteria into the task spec source.
|
|
49
|
-
- For multi-PR work, resume from the
|
|
49
|
+
- For multi-PR work, resume from the next approved executable checkpoint in the roadmap using [PR continuation](references/pr-continuation.md) to resolve its base, dependencies and runtime capability, then select its first pending task. Hand off ready checkpoints without modifying them; awaiting merge alone does not make them the next execution target. For single-PR work, the canonical name worktree/branch is enough and the roadmap collapses to one checkpoint.
|
|
50
50
|
|
|
51
51
|
## Pull request lifecycle
|
|
52
52
|
|
|
53
|
-
1. Start from
|
|
53
|
+
1. Start independent work from updated production. For multi-PR work or a Git dependency, choose the verified base using [PR continuation](references/pr-continuation.md). Use the canonical worktree/branch and the shared PR-scope rule in [Worktree and PR lifecycle](../orchestrator/SKILL.md#worktree-and-pr-lifecycle); formal SDD does not define a separate size policy.
|
|
54
54
|
2. Implement one coherent first slice, commit it, push the work branch, and open the PR immediately as draft with `gh pr create --draft`.
|
|
55
55
|
3. Continue implementation, commits and pushes only while the PR is draft. Draft means the code can still change; ready means the current SHA is the candidate to merge.
|
|
56
56
|
4. Before ready, complete every applicable preflight item: code, version bump, local tests, project quality command (`pnpm qa:quality` when defined), Vercel preview review when the project uses Vercel, final diff inspection, and full PR review.
|
|
@@ -58,7 +58,7 @@ Use this section only after routing selects formal SDD work, including work prom
|
|
|
58
58
|
6. Never push to a ready PR. If it needs changes, first run `gh pr ready --undo <number>`, then modify and push while draft, repeat preflight and review revalidation, mark ready again, and wait for a fresh complete gate when checks are configured. Use [coverage revalidation](../xreview/SKILL.md#7-revalidate-coverage-and-stop), not an automatic repeated panel; preserve evidence only for still-valid contracts and integration assumptions, including the effective base.
|
|
59
59
|
7. Merge only after explicit user approval. When PR checks are configured, their passing result must match the current candidate SHA.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Development order and merge order are distinct. A ready checkpoint does not block the next independent or permitted Git-stacked checkpoint; an unavailable external prerequisite can. Record the dependencies and merge order in the existing roadmap and follow the continuation reference. Preserve project opt-outs and real supervisor/capability limits, and never turn plan approval into merge authorization.
|
|
62
62
|
|
|
63
63
|
## HTML review view (on demand)
|
|
64
64
|
|
|
@@ -89,7 +89,7 @@ If the project manages work through an issue tracker, issues (`to-issues`) take
|
|
|
89
89
|
|
|
90
90
|
1. Read `work/{name}/plan.md` — the board says what's done and what's pending, and its `Spec` column names each formal task's only source.
|
|
91
91
|
2. Resolve the declared Spec following the Delegation handoff rule in Executing, including its binding-before-get and scoped lookup requirements. For Markdown, read the canonical absolute path established there; use `mem_context` for recent phase outcomes.
|
|
92
|
-
3.
|
|
92
|
+
3. Select the next approved executable checkpoint using [PR continuation](references/pr-continuation.md) for multi-PR work or a Git dependency, then continue its first pending task. Preserve ready checkpoints unchanged; stop only when no approved work can safely proceed.
|
|
93
93
|
|
|
94
94
|
## Closing
|
|
95
95
|
|
|
@@ -45,12 +45,13 @@ Every formal task has exactly one recoverable `Spec` reference in the plan table
|
|
|
45
45
|
> Full checkpoint history lives in Engram under `work/[name]/pr/[NN]`.
|
|
46
46
|
> Every evidence entry cites the relevant `SC-*` criteria it proves and records the command/setup, scope, result and limits.
|
|
47
47
|
> PR status advances: ⬜ Pending → 📝 Draft → 🔍 Reviewed → ⏳ Ready / gates when configured → ✅ Merged.
|
|
48
|
-
> If a ready PR changes, return it to Draft with `gh pr ready --undo`,
|
|
48
|
+
> If a ready PR needs changes or retargeting, return it to Draft with `gh pr ready --undo`, revalidate affected review coverage and configured gates, and record the new candidate/base context. A changed base or integration context matters even with the same head SHA. If no PR checks are configured, confirm and record that fact; an empty `gh pr checks` immediately after ready does not prove absence. Recheck candidate and base context immediately before reporting or merging.
|
|
49
|
+
> For multiple PRs, use this same roadmap to record base branch/SHA, parent PR or external prerequisite, merge order and the real blocker where applicable. Ready permits continuing approved safe work, not merging; see [PR continuation](pr-continuation.md). Preserve project restrictions and actual runtime capabilities.
|
|
49
50
|
|
|
50
|
-
| PR | Scope | Branch | Worktree | Base | Status | Merge evidence |
|
|
51
|
-
|
|
52
|
-
| 01 | [scope] | [branch] | [worktree] | [base] | ⬜ | [evidence] |
|
|
53
|
-
| 02 | [scope] | [branch] | [worktree] | [base] | ⬜ | [evidence] |
|
|
51
|
+
| PR | Scope | Branch | Worktree | Base / prerequisite / merge order | Status / blocker | Merge evidence |
|
|
52
|
+
|----|-------|--------|----------|-----------------------------------|------------------|----------------|
|
|
53
|
+
| 01 | [scope] | [branch] | [worktree] | [base branch@SHA; dependency and order if relevant] | ⬜ | [evidence] |
|
|
54
|
+
| 02 | [scope] | [branch] | [worktree] | [base branch@SHA; dependency and order if relevant] | ⬜ | [evidence] |
|
|
54
55
|
|
|
55
56
|
> Intermediate PRs do not delete `work/[name]/`; only the final close does.
|
|
56
57
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Continue approved work across PRs
|
|
2
|
+
|
|
3
|
+
Read this reference for multi-PR work or dependencies on an unmerged PR. It uses the existing roadmap, worktrees and review evidence; it does not create another queue, registry or permission to merge.
|
|
4
|
+
|
|
5
|
+
## Choose the next runnable checkpoint
|
|
6
|
+
|
|
7
|
+
- **Independent work:** use updated production as the base when no unmerged diff is needed.
|
|
8
|
+
- **Git dependency:** use a separate child branch/worktree from a verified stable parent candidate when project rules and available tools permit it. Target the child PR at that parent branch so its review shows its own change. Do not modify a ready parent to advance its child.
|
|
9
|
+
- **External prerequisite:** verify the actual artifact, migrated database, deployment or decision the next contract needs. A missing prerequisite blocks that consumer, not another approved independent checkpoint. Do not classify all frontend work as independent or all database work as blocked.
|
|
10
|
+
|
|
11
|
+
Record the selected branch/base SHA, parent PR or external prerequisite, merge order and any real blocker in the existing roadmap columns. Keep readiness and integration separate: a child may be ready for review against its parent without being ready to merge to production. Do not invent new work, dependencies or an arbitrary chain length just to keep going.
|
|
12
|
+
|
|
13
|
+
## Preserve the chain and its evidence
|
|
14
|
+
|
|
15
|
+
Keep ready parents immutable. If a parent changes, a base advances, or a PR is retargeted, inspect the actual base/head/merge-base and effective diff. The same head SHA does not prove that previous review or checks still cover the integration context.
|
|
16
|
+
|
|
17
|
+
Before deliberately changing or retargeting a ready child, return it to draft. Use the project's authorized Git strategy, preserve unrelated edits, and revalidate the affected coverage under `xreview`, plus the applicable local checks and fresh gates. Do not rewrite parents, discard work or force shared history merely for convenience; ask before a history-changing operation that is not already authorized.
|
|
18
|
+
|
|
19
|
+
After a parent merges, inspect the child's real target. Retarget it to the appropriate integration base when needed before any merge; never accidentally merge into an open parent or an abandoned branch. Hosting-provider auto-retarget behavior is conditional, not a guarantee: do not delete branches to force it. A retarget event alone is not evidence that CI reran; verify the configured triggers and complete the proper draft/ready gate cycle.
|
|
20
|
+
|
|
21
|
+
## Approval and stopping conditions
|
|
22
|
+
|
|
23
|
+
Merge only on an explicit user order identifying the PR or intended batch, in a dependency-safe order. Plan approval, a ready child, passing checks or permission to continue development never authorizes future merges. Recheck candidate and base context immediately before the merge.
|
|
24
|
+
|
|
25
|
+
After reporting a ready checkpoint, continue only approved work that is safe and verifiable. Stop when that scope is exhausted, a material decision is missing, or available capabilities cannot execute the next safe work. Keep the work artifacts and report what is ready, what is blocked and why; do not call the overall roadmap complete merely because one PR is ready.
|
|
26
|
+
|
|
27
|
+
**Capability boundary:** each runtime requires its actual available Git/PR tools and permissions, not an assumed API or background supervisor. Report unavailable capabilities instead of bypassing them; this policy alone does not provide unattended execution or guarantee uninterrupted sessions.
|
|
@@ -130,7 +130,7 @@ Every piece of information about a piece of work has exactly ONE home — never
|
|
|
130
130
|
- Commit per task or per bounded group of tasks — small, separate commits whose history maps to the work; never everything in one giant commit.
|
|
131
131
|
- Never push code or behavior changes directly to production branches (main/master or the repo's protected/release branches): those always go through a work branch + PR. Pushing a work branch or a worktree branch is fine without asking.
|
|
132
132
|
- Exception: TRIVIAL changes — docs, typos, content removal, config text with no behavior or code-logic impact — may be committed and pushed directly to the production branch. When in doubt about whether a change is trivial, it is not: use a PR.
|
|
133
|
-
- Start each non-trivial PR from
|
|
133
|
+
- Start each non-trivial PR in its canonical worktree/branch from the verified base appropriate to its dependencies: updated production for independent work, or a permitted stable parent candidate for a Git dependency. Keep one concrete objective per PR and record base/SHA, parent or external prerequisite, and merge order in the existing plan. Ready does not by itself stop approved safe work; follow the orchestrator's continuation rule, respecting project restrictions and runtime capabilities. Parents ready remain immutable and every merge still requires explicit user approval.
|
|
134
134
|
- After the first coherent commit, push the work branch and always open the PR as draft with `gh pr create --draft`. Creating and pushing the work branch and draft PR is fine without asking.
|
|
135
135
|
- Keep the PR in draft while its code can still change. All subsequent commits and pushes happen while draft; never push to a ready PR.
|
|
136
136
|
- Before ready, complete all applicable preflight work: code, version bump, local tests, the project's quality command (`pnpm qa:quality` when defined), Vercel preview review when the project uses Vercel, final diff inspection, and the full PR review. React Doctor is manual/local, not a GitHub Actions gate.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Goal Mode — manage persistent long-running objectives
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
The Goal Mode plugin should handle `/goal $ARGUMENTS` before this prompt runs.
|
|
6
|
-
|
|
7
|
-
If you are seeing this message as a normal assistant prompt, report that Goal Mode did not intercept the slash command. Do not start work, edit files, run tools, create branches, open PRs, or change repository state.
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { GoalStore } from "./types.js";
|
|
4
|
-
|
|
5
|
-
export interface MasterArtifactsInput {
|
|
6
|
-
store: GoalStore;
|
|
7
|
-
goalId: string;
|
|
8
|
-
rootDir: string;
|
|
9
|
-
allowedRootDir?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface MasterArtifactsResult {
|
|
13
|
-
created: boolean;
|
|
14
|
-
preserved: boolean;
|
|
15
|
-
prdPath: string;
|
|
16
|
-
planPath: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createMasterArtifacts(input: MasterArtifactsInput): MasterArtifactsResult {
|
|
20
|
-
const goal = input.store.getGoal(input.goalId);
|
|
21
|
-
if (!goal) {
|
|
22
|
-
throw new Error(`Goal ${input.goalId} not found.`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const allowedRootDir = input.allowedRootDir ?? input.rootDir;
|
|
26
|
-
assertSafeArtifactPath(input.rootDir, allowedRootDir, "Goal artifact root");
|
|
27
|
-
fs.mkdirSync(input.rootDir, { recursive: true });
|
|
28
|
-
|
|
29
|
-
const prdPath = path.join(input.rootDir, "PRD.md");
|
|
30
|
-
const planPath = path.join(input.rootDir, "plan.md");
|
|
31
|
-
assertSafeArtifactPath(prdPath, allowedRootDir, "Goal PRD artifact");
|
|
32
|
-
assertSafeArtifactPath(planPath, allowedRootDir, "Goal plan artifact");
|
|
33
|
-
const prdCreated = writeIfMissing(prdPath, renderMasterPrd(goal.objective));
|
|
34
|
-
const planCreated = writeIfMissing(planPath, renderMasterPlan(goal.objective));
|
|
35
|
-
|
|
36
|
-
input.store.recordArtifact(goal.id, { kind: "prd", path: prdPath });
|
|
37
|
-
input.store.recordArtifact(goal.id, { kind: "plan", path: planPath });
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
created: prdCreated || planCreated,
|
|
41
|
-
preserved: !prdCreated || !planCreated,
|
|
42
|
-
prdPath,
|
|
43
|
-
planPath,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function assertSafeArtifactPath(filePath: string, allowedRootDir: string, label = "Goal artifact"): void {
|
|
48
|
-
const resolvedAllowedRoot = resolveExistingPathWithoutSymlinks(allowedRootDir, `${label} root`);
|
|
49
|
-
const resolvedPath = resolveExistingPathWithoutSymlinks(filePath, label);
|
|
50
|
-
if (!isContainedIn(resolvedPath, resolvedAllowedRoot)) {
|
|
51
|
-
throw new Error(`${label} must stay inside ${allowedRootDir}. Refusing: ${filePath}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const stats = lstatIfExists(filePath);
|
|
55
|
-
if (stats?.isFile() && stats.nlink > 1) {
|
|
56
|
-
throw new Error(`${label} must not be a hard link.`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function writeIfMissing(filePath: string, content: string): boolean {
|
|
61
|
-
if (fs.existsSync(filePath)) return false;
|
|
62
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function resolveExistingPathWithoutSymlinks(input: string, label: string): string {
|
|
67
|
-
assertNoSymlinkInExistingPath(input, label);
|
|
68
|
-
let current = path.resolve(input);
|
|
69
|
-
const missing: string[] = [];
|
|
70
|
-
while (!fs.existsSync(current)) {
|
|
71
|
-
missing.push(path.basename(current));
|
|
72
|
-
const parent = path.dirname(current);
|
|
73
|
-
if (parent === current) break;
|
|
74
|
-
current = parent;
|
|
75
|
-
}
|
|
76
|
-
const real = fs.existsSync(current) ? fs.realpathSync(current) : current;
|
|
77
|
-
return missing.reduceRight((base, part) => path.join(base, part), real);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function assertNoSymlinkInExistingPath(input: string, label: string): void {
|
|
81
|
-
const resolved = path.resolve(input);
|
|
82
|
-
const root = path.parse(resolved).root;
|
|
83
|
-
const relativeParts = path.relative(root, resolved).split(path.sep).filter(Boolean);
|
|
84
|
-
let current = root;
|
|
85
|
-
for (const part of relativeParts) {
|
|
86
|
-
current = path.join(current, part);
|
|
87
|
-
const stats = lstatIfExists(current);
|
|
88
|
-
if (!stats) return;
|
|
89
|
-
if (stats.isSymbolicLink()) {
|
|
90
|
-
throw new Error(`${label} must not include symlinks.`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function lstatIfExists(input: string): fs.Stats | undefined {
|
|
96
|
-
try {
|
|
97
|
-
return fs.lstatSync(input);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function isContainedIn(candidate: string, root: string): boolean {
|
|
105
|
-
const relative = path.relative(root, candidate);
|
|
106
|
-
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function renderMasterPrd(objective: string): string {
|
|
110
|
-
return [
|
|
111
|
-
"# PRD maestro",
|
|
112
|
-
"",
|
|
113
|
-
"## Objetivo",
|
|
114
|
-
"",
|
|
115
|
-
objective,
|
|
116
|
-
"",
|
|
117
|
-
"## Alcance",
|
|
118
|
-
"",
|
|
119
|
-
"- Mantener el objetivo global del Goal Mode.",
|
|
120
|
-
"- Dividir el trabajo en slices ejecutables por el orquestador.",
|
|
121
|
-
"- Esperar merges manuales antes de continuar.",
|
|
122
|
-
"",
|
|
123
|
-
].join("\n");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function renderMasterPlan(objective: string): string {
|
|
127
|
-
return [
|
|
128
|
-
"# Plan maestro",
|
|
129
|
-
"",
|
|
130
|
-
"## Objetivo",
|
|
131
|
-
"",
|
|
132
|
-
objective,
|
|
133
|
-
"",
|
|
134
|
-
"## Fases",
|
|
135
|
-
"",
|
|
136
|
-
"1. Preparar PRD/plan maestro.",
|
|
137
|
-
"2. Ejecutar slices acotados con el orquestador.",
|
|
138
|
-
"3. Procesar reviews y esperar merge externo cuando corresponda.",
|
|
139
|
-
"4. Verificar criterios globales antes de cerrar.",
|
|
140
|
-
"",
|
|
141
|
-
].join("\n");
|
|
142
|
-
}
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { GoalRecord, GoalStatus, GoalStore, NextAction } from "./types.js";
|
|
4
|
-
import { assertSafeArtifactPath, createMasterArtifacts } from "./artifacts.js";
|
|
5
|
-
|
|
6
|
-
const COMMANDS = new Set(["status", "plan", "history", "pause", "resume", "cancel", "merged"]);
|
|
7
|
-
const EXPLICITLY_UNSUPPORTED = new Set(["quick", "work"]);
|
|
8
|
-
|
|
9
|
-
export interface GoalCommandHandlersOptions {
|
|
10
|
-
store: GoalStore;
|
|
11
|
-
project: string;
|
|
12
|
-
artifactsRootDir?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface GoalCommandResponse {
|
|
16
|
-
message: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface GoalCommandHandlers {
|
|
20
|
-
handleGoalCommand(input: string): GoalCommandResponse;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createGoalCommandHandlers(options: GoalCommandHandlersOptions): GoalCommandHandlers {
|
|
24
|
-
let currentGoalId: string | undefined;
|
|
25
|
-
|
|
26
|
-
const currentGoal = () => {
|
|
27
|
-
const byId = currentGoalId ? options.store.getGoal(currentGoalId) : undefined;
|
|
28
|
-
const active = byId ?? options.store.getCurrentGoal(options.project);
|
|
29
|
-
currentGoalId = active?.id ?? currentGoalId;
|
|
30
|
-
return active;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const requireCurrentGoal = () => {
|
|
34
|
-
const goal = currentGoal();
|
|
35
|
-
if (!goal) {
|
|
36
|
-
throw new Error("No active goal found. Start one with /goal <objective>.");
|
|
37
|
-
}
|
|
38
|
-
return goal;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
handleGoalCommand(rawInput: string): GoalCommandResponse {
|
|
43
|
-
const input = rawInput.trim();
|
|
44
|
-
const [firstToken = "", ...args] = input.split(/\s+/);
|
|
45
|
-
const firstLower = firstToken.toLowerCase();
|
|
46
|
-
const normalized = firstLower === "merged" ? "merged" : input.toLowerCase();
|
|
47
|
-
|
|
48
|
-
if (input.length === 0) {
|
|
49
|
-
throw new Error("Goal objective cannot be empty.");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (EXPLICITLY_UNSUPPORTED.has(normalized)) {
|
|
53
|
-
throw new Error("/goal quick and /goal work are not supported. Use /goal <objective> for large goals.");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!COMMANDS.has(normalized)) {
|
|
57
|
-
let artifactRootDir: string | undefined;
|
|
58
|
-
const goal = options.store.transaction(() => {
|
|
59
|
-
const createdGoal = options.store.createGoal({
|
|
60
|
-
objective: input,
|
|
61
|
-
project: options.project,
|
|
62
|
-
});
|
|
63
|
-
artifactRootDir = options.artifactsRootDir
|
|
64
|
-
? path.join(options.artifactsRootDir, createdGoal.id)
|
|
65
|
-
: undefined;
|
|
66
|
-
try {
|
|
67
|
-
if (artifactRootDir) {
|
|
68
|
-
createMasterArtifacts({
|
|
69
|
-
store: options.store,
|
|
70
|
-
goalId: createdGoal.id,
|
|
71
|
-
rootDir: artifactRootDir,
|
|
72
|
-
allowedRootDir: options.artifactsRootDir,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
options.store.appendEvent(createdGoal.id, {
|
|
76
|
-
type: "goal.created",
|
|
77
|
-
message: `Goal created: ${createdGoal.objective}`,
|
|
78
|
-
data: { objective: createdGoal.objective, project: createdGoal.project },
|
|
79
|
-
});
|
|
80
|
-
} catch (error) {
|
|
81
|
-
let cleanupFailure: string | undefined;
|
|
82
|
-
if (artifactRootDir && options.artifactsRootDir) {
|
|
83
|
-
cleanupFailure = cleanupBootstrappedArtifactDir(artifactRootDir, options.artifactsRootDir);
|
|
84
|
-
}
|
|
85
|
-
const rootCause = error instanceof Error ? error.message : String(error);
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Goal bootstrap failed: ${rootCause}${cleanupFailure ? ` Cleanup also failed: ${cleanupFailure}` : ""}`,
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
return createdGoal;
|
|
91
|
-
});
|
|
92
|
-
currentGoalId = goal.id;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
message: `Goal created: ${goal.objective}\nStatus: ${goal.status}\nNext action: ${formatNextAction(options.store.nextAction(goal.id))}`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (normalized === "status") return statusResponse(currentGoal(), options.store);
|
|
100
|
-
if (normalized === "plan") {
|
|
101
|
-
return planResponse(requireCurrentGoal(), options.store, options.artifactsRootDir);
|
|
102
|
-
}
|
|
103
|
-
if (normalized === "history") return historyResponse(requireCurrentGoal(), options.store);
|
|
104
|
-
|
|
105
|
-
const goal = requireCurrentGoal();
|
|
106
|
-
if (normalized === "pause") {
|
|
107
|
-
return transitionResponse(options.store, goal, "paused", "Goal paused by user.");
|
|
108
|
-
}
|
|
109
|
-
if (normalized === "resume") {
|
|
110
|
-
if (goal.status === "waiting_for_merge") {
|
|
111
|
-
throw new Error("Cannot resume while the goal is waiting for an external PR merge.");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const nextStatus = options.store.getOpenPullRequest(goal.id) ? "waiting_for_merge" : "active";
|
|
115
|
-
return transitionResponse(options.store, goal, nextStatus, "Goal resumed by user.");
|
|
116
|
-
}
|
|
117
|
-
if (normalized === "cancel") {
|
|
118
|
-
return transitionResponse(options.store, goal, "cancelled", "Goal cancelled by user.");
|
|
119
|
-
}
|
|
120
|
-
if (normalized === "merged") {
|
|
121
|
-
return mergedResponse(options.store, goal, args.join(" "));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
throw new Error(`Unsupported /goal command: ${input}`);
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function statusResponse(goal: GoalRecord | undefined, store: GoalStore): GoalCommandResponse {
|
|
130
|
-
if (!goal) {
|
|
131
|
-
return { message: "No active goal. Start one with /goal <objective>." };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const lines = [
|
|
135
|
-
`Goal: ${goal.objective}`,
|
|
136
|
-
`Status: ${goal.status}`,
|
|
137
|
-
`Next action: ${formatNextAction(store.nextAction(goal.id))}`,
|
|
138
|
-
];
|
|
139
|
-
const issue = latestAutoContinueIssue(store, goal.id);
|
|
140
|
-
if (issue) lines.push(`Operational issue: ${issue.message}`);
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
message: lines.join("\n"),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function planResponse(goal: GoalRecord, store: GoalStore, artifactsRootDir: string | undefined): GoalCommandResponse {
|
|
148
|
-
const plan = store.getArtifact(goal.id, "plan");
|
|
149
|
-
if (plan) {
|
|
150
|
-
if (!fs.existsSync(plan.path)) {
|
|
151
|
-
throw new Error(`Registered goal plan is not readable at ${plan.path}. Recreate or fix the artifact path.`);
|
|
152
|
-
}
|
|
153
|
-
if (artifactsRootDir) {
|
|
154
|
-
assertSafeArtifactPath(plan.path, artifactsRootDir, "Registered goal plan");
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
message: [`Plan for goal: ${goal.objective}`, fs.readFileSync(plan.path, "utf8")].join("\n\n"),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (artifactsRootDir) {
|
|
162
|
-
throw new Error("Goal master plan artifact is not registered. Recreate or repair the goal artifacts.");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
message: [
|
|
167
|
-
`Plan for goal: ${goal.objective}`,
|
|
168
|
-
"Phases: master PRD/plan generation, slice execution, PR review, waiting for merge, final verification.",
|
|
169
|
-
"Detailed master artifacts are created by the next Goal Mode slice.",
|
|
170
|
-
].join("\n"),
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function cleanupBootstrappedArtifactDir(rootDir: string, allowedRootDir: string): string | undefined {
|
|
175
|
-
try {
|
|
176
|
-
assertSafeArtifactPath(rootDir, allowedRootDir, "Goal artifact cleanup path");
|
|
177
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
178
|
-
return undefined;
|
|
179
|
-
} catch (error) {
|
|
180
|
-
return error instanceof Error ? error.message : String(error);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function mergedResponse(store: GoalStore, goal: GoalRecord, mergeCommit: string): GoalCommandResponse {
|
|
185
|
-
const pullRequest = store.getOpenPullRequest(goal.id);
|
|
186
|
-
if (!pullRequest) {
|
|
187
|
-
throw new Error("No open pull request is waiting for merge.");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const merged = store.recordPullRequestMerged(pullRequest.id, {
|
|
191
|
-
mergedAt: new Date().toISOString(),
|
|
192
|
-
mergeCommit: mergeCommit.trim() || "manual",
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
message: `Pull request #${merged.number} marked as merged. Goal status: ${store.getGoal(goal.id)?.status ?? "unknown"}`,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function historyResponse(goal: GoalRecord, store: GoalStore): GoalCommandResponse {
|
|
201
|
-
const events = store.listEvents(goal.id);
|
|
202
|
-
const lines = events.length === 0
|
|
203
|
-
? ["No history events recorded yet."]
|
|
204
|
-
: events.map((event) => `- ${event.createdAt} ${event.type}: ${event.message}`);
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
message: [`History for goal: ${goal.objective}`, ...lines].join("\n"),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function transitionResponse(
|
|
212
|
-
store: GoalStore,
|
|
213
|
-
goal: GoalRecord,
|
|
214
|
-
status: GoalStatus,
|
|
215
|
-
reason: string,
|
|
216
|
-
): GoalCommandResponse {
|
|
217
|
-
const updated = store.transitionGoal(goal.id, status, { reason });
|
|
218
|
-
return {
|
|
219
|
-
message: `Goal ${updated.status}: ${updated.objective}`,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function latestAutoContinueIssue(store: GoalStore, goalId: string) {
|
|
224
|
-
const events = store.listEvents(goalId);
|
|
225
|
-
const currentStateSequence = Math.max(
|
|
226
|
-
0,
|
|
227
|
-
...events
|
|
228
|
-
.filter((event) => !event.type.startsWith("goal.auto_continue_"))
|
|
229
|
-
.map((event) => event.sequence),
|
|
230
|
-
);
|
|
231
|
-
return events
|
|
232
|
-
.filter((event) =>
|
|
233
|
-
(
|
|
234
|
-
event.type === "goal.auto_continue_unavailable" ||
|
|
235
|
-
event.type === "goal.auto_continue_failed" ||
|
|
236
|
-
event.type === "goal.auto_continue_skipped"
|
|
237
|
-
) &&
|
|
238
|
-
readEventStateSequence(event.data) === currentStateSequence,
|
|
239
|
-
)
|
|
240
|
-
.at(-1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function readEventStateSequence(data: unknown): number | undefined {
|
|
244
|
-
if (typeof data !== "object" || data === null || Array.isArray(data)) return undefined;
|
|
245
|
-
const value = (data as { stateSequence?: unknown }).stateSequence;
|
|
246
|
-
return typeof value === "number" ? value : undefined;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function formatNextAction(action: NextAction): string {
|
|
250
|
-
if (action.type === "wait_for_merge") {
|
|
251
|
-
return `waiting for external merge of ${action.pullRequestId}`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return "continue";
|
|
255
|
-
}
|