pirl-framework-cli 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -2
  4. package/bundled-framework/files/.agents/evals/layer_record.py +438 -0
  5. package/bundled-framework/files/.agents/evals/tests/test_layer_record.py +264 -0
  6. package/bundled-framework/files/.agents/evals/tests/test_skills.py +210 -0
  7. package/bundled-framework/files/.agents/evals/tests/test_validate_layer_artifact.py +214 -0
  8. package/bundled-framework/files/.agents/evals/validate_layer_artifact.py +415 -0
  9. package/bundled-framework/files/.agents/instructions/implement.instructions.md +343 -0
  10. package/bundled-framework/files/.agents/instructions/layered-task-execution.instructions.md +370 -0
  11. package/bundled-framework/files/.agents/instructions/layout-verification.instructions.md +107 -0
  12. package/bundled-framework/files/.agents/instructions/plan.instructions.md +292 -0
  13. package/bundled-framework/files/.agents/instructions/review.instructions.md +236 -0
  14. package/bundled-framework/files/.agents/instructions/self-evaluation.instructions.md +146 -0
  15. package/bundled-framework/files/.agents/instructions/standalone-and-maintenance.instructions.md +230 -0
  16. package/bundled-framework/files/.agents/roles/backend-developer.role.md +45 -0
  17. package/bundled-framework/files/.agents/roles/business-analyst.role.md +55 -0
  18. package/bundled-framework/files/.agents/roles/code-analyst.role.md +63 -0
  19. package/bundled-framework/files/.agents/roles/code-reviewer.role.md +46 -0
  20. package/bundled-framework/files/.agents/roles/frontend-developer.role.md +51 -0
  21. package/bundled-framework/files/.agents/roles/process-auditor.role.md +62 -0
  22. package/bundled-framework/files/.agents/roles/solution-architect.role.md +53 -0
  23. package/bundled-framework/files/.agents/roles/technical-writer.role.md +68 -0
  24. package/bundled-framework/files/.agents/roles/ux-designer.role.md +46 -0
  25. package/bundled-framework/files/.skills/pirl-implement/SKILL.md +115 -0
  26. package/bundled-framework/files/.skills/pirl-implement/assets/implementation-record-template.md +22 -0
  27. package/bundled-framework/files/.skills/pirl-implement/references/implementation-record-format.md +41 -0
  28. package/bundled-framework/files/.skills/pirl-implement/scripts/add_implementation_record.py +58 -0
  29. package/bundled-framework/files/.skills/pirl-implement/scripts/new_layout_run.py +51 -0
  30. package/bundled-framework/files/.skills/pirl-plan/SKILL.md +100 -0
  31. package/bundled-framework/files/.skills/pirl-plan/assets/plan-template.md +48 -0
  32. package/bundled-framework/files/.skills/pirl-plan/references/plan-format.md +65 -0
  33. package/bundled-framework/files/.skills/pirl-plan/scripts/new_plan.py +93 -0
  34. package/bundled-framework/files/.skills/pirl-review/SKILL.md +106 -0
  35. package/bundled-framework/files/.skills/pirl-review/assets/review-template.md +92 -0
  36. package/bundled-framework/files/.skills/pirl-review/references/review-format.md +70 -0
  37. package/bundled-framework/files/.skills/pirl-review/scripts/new_review.py +83 -0
  38. package/bundled-framework/files/AGENTS.md +303 -0
  39. package/bundled-framework/files/CONTEXT.md +187 -0
  40. package/bundled-framework/files/LICENSE +21 -0
  41. package/bundled-framework/files/README.md +170 -0
  42. package/bundled-framework/files/gitignore.pirl-bundled +12 -0
  43. package/bundled-framework/manifest.json +45 -0
  44. package/dist/bin/main.js +128 -0
  45. package/dist/bin/pirl.js +6 -0
  46. package/dist/commands/dispatch.js +57 -0
  47. package/dist/commands/registry.js +39 -0
  48. package/dist/core/clipboard.js +70 -0
  49. package/dist/core/config.js +73 -0
  50. package/dist/core/exec.js +32 -0
  51. package/dist/core/paths.js +34 -0
  52. package/dist/core/targetRepo.js +22 -0
  53. package/dist/doctor/doctor.js +95 -0
  54. package/dist/engine/agentMerge.js +71 -0
  55. package/dist/engine/agents.js +67 -0
  56. package/dist/engine/attachments.js +251 -0
  57. package/dist/engine/claudeQuota.js +62 -0
  58. package/dist/engine/claudeRunner.js +118 -0
  59. package/dist/engine/claudeStream.js +205 -0
  60. package/dist/engine/hubData.js +176 -0
  61. package/dist/engine/permissions.js +67 -0
  62. package/dist/engine/pricing.js +24 -0
  63. package/dist/engine/protocol.js +98 -0
  64. package/dist/engine/run.js +527 -0
  65. package/dist/engine/runStore.js +67 -0
  66. package/dist/engine/types.js +3 -0
  67. package/dist/hub/framework.js +13 -0
  68. package/dist/hub/hub.js +125 -0
  69. package/dist/hub/services.js +38 -0
  70. package/dist/hub/update.js +261 -0
  71. package/dist/providers/claude.js +62 -0
  72. package/dist/providers/index.js +17 -0
  73. package/dist/providers/types.js +1 -0
  74. package/dist/ui/App.js +849 -0
  75. package/dist/ui/browse.js +144 -0
  76. package/dist/ui/components/Header.js +54 -0
  77. package/dist/ui/components/Input.js +76 -0
  78. package/dist/ui/components/Lines.js +50 -0
  79. package/dist/ui/components/Panels.js +73 -0
  80. package/dist/ui/content.js +225 -0
  81. package/dist/ui/frameworkConsole.js +541 -0
  82. package/dist/ui/inputEditor.js +86 -0
  83. package/dist/ui/layout.js +36 -0
  84. package/dist/ui/lines.js +15 -0
  85. package/dist/ui/listOverlay.js +97 -0
  86. package/dist/ui/runView.js +305 -0
  87. package/dist/ui/session.js +1 -0
  88. package/dist/ui/terminalInput.js +45 -0
  89. package/dist/ui/text.js +58 -0
  90. package/dist/ui/theme.js +139 -0
  91. package/dist/ui/ticker.js +48 -0
  92. package/package.json +67 -4
@@ -0,0 +1,170 @@
1
+ # PIRL Framework
2
+
3
+ **Plan – Implement – Review Layer**: a Hub that coordinates software development
4
+ work for AI Agents, running entirely on instruction files.
5
+
6
+ You assign work in plain words. The Agent reads [`AGENTS.md`](AGENTS.md), picks
7
+ the right layer, splits the work among specialist roles, records the results as
8
+ files in the repository, then improves the instruction files themselves after
9
+ each run.
10
+
11
+ ## What the framework does
12
+
13
+ - **Three layers, three skills.** Plan settles the work to do and the quality
14
+ gates (QG). Implement edits the source according to the Plan. Review checks
15
+ the result against the QGs by reasoning on the source. Each layer is run
16
+ through its skill in `.skills/` (`pirl-plan`, `pirl-implement`, `pirl-review`).
17
+ - **Any repository.** The product repositories are not part of the Hub. You
18
+ name the target repository of each request; the Agent reads that
19
+ repository's own context documentation (its `AGENTS.md`, `CLAUDE.md`, README
20
+ and similar).
21
+ - **AI Agent roles.** Each layer assigns work to roles with the right expertise,
22
+ for example `business-analyst`, `backend-developer`, `code-reviewer`.
23
+ - **Self-improvement.** At the end of each layer run, the `process-auditor` role
24
+ finds each role's mistakes, fixes the relevant instruction files at the root,
25
+ writes a JSONL record and reports what was fixed and where.
26
+
27
+ The framework does not build, does not run the application for acceptance and
28
+ does not deploy. Those belong to your build/test/release stage. Implement only
29
+ writes and runs functional tests related to the request, and captures layout
30
+ with mocks when the UI changes.
31
+
32
+ ## Requirements
33
+
34
+ - An AI Agent that can read files in the repository and run commands, for
35
+ example Claude Code. If the host loads `AGENTS.md` automatically, it works
36
+ right away; if not, ask the Agent to read `AGENTS.md` before assigning work.
37
+ - Git.
38
+ - Python 3.9 or later, for the validator, the record tool and the skills' scripts.
39
+
40
+ ## Quick start
41
+
42
+ ### 1. Get the Hub
43
+
44
+ ```powershell
45
+ git clone https://github.com/tdmadc107/PIRL-framework.git
46
+ ```
47
+
48
+ ### 2. Have a target repository ready
49
+
50
+ Any local Git repository works; nothing needs registering. Give its path in your
51
+ request, relative to the Hub root or absolute. If you want to keep clones
52
+ inside the Hub, put them in `.repos/`, which Git ignores.
53
+
54
+ The Agent learns the repository from the repository itself: architecture,
55
+ conventions, the command that runs tests by file or name, UI languages and
56
+ viewports, known gaps. If the repository has no such document yet, you can
57
+ assign the Agent to write one (for example its `AGENTS.md`) as a standalone
58
+ task.
59
+
60
+ ### 3. Assign work
61
+
62
+ No special commands or keywords are needed; just say it:
63
+
64
+ | You want to | Example request | The Agent runs |
65
+ | --- | --- | --- |
66
+ | Plan | "Create a Plan for `../shop-web`: customers get an email when an order is cancelled" | `pirl-plan` skill |
67
+ | Implement | "Implement Plan T01 in `<package>`" | `pirl-implement` skill |
68
+ | Review | "Review the implementation of Plan T01" | `pirl-review` skill |
69
+ | Fix per a Review | "Fix per report `<report path>`" | `pirl-implement` skill, repair run |
70
+ | Small work outside the chain | "Update the AGENTS.md of shop-web" | Standalone task |
71
+ | Edit the framework itself | "Fix the cleanup rule in the router" | Framework maintenance |
72
+ | Just ask | "Which tasks does Plan T01 depend on?" | Answer, no files created |
73
+
74
+ Each layer runs only when you ask for it; the Agent does not move on to the next
75
+ layer by itself. The Agent picks the smallest team of roles with enough
76
+ expertise. To choose the team yourself, name the roles in your request; the role
77
+ catalog is in the [standalone and maintenance instructions](.agents/instructions/standalone-and-maintenance.instructions.md#role-catalog).
78
+
79
+ Plans and reports go where you say. If you name no location, they are created
80
+ under the Hub root. Inside the Hub, Git ignores Plan packages and their reports
81
+ (directories named `<request-slug>_<YYYYMMDD-HHmmss>/`), because they describe
82
+ your projects.
83
+
84
+ ## A full working cycle
85
+
86
+ 1. **Plan.** The Agent investigates the source and asks when a product decision
87
+ is missing. The result is a Plan package `<request-slug>_<YYYYMMDD-HHmmss>/`,
88
+ one file per task with: What to do (REQs), How to do it, Participating roles,
89
+ Required quality gates (QGs). Read and settle the Plan before implementing.
90
+ 2. **Implement.** The Agent edits the source in the target repository, within
91
+ the Plan's scope. Verification roles conclude each QG by reasoning on the
92
+ source. The results go into the `Implementation Record` section at the end of
93
+ the Plan file. When the UI changes, layout screenshots are in
94
+ `.tmp/layout-snapshots/` (not in git).
95
+ 3. **Review.** The Agent checks each QG against the source and writes a report
96
+ `<plan-set>/<task>/round-<NN>-review-...md`; with the default location it
97
+ sits inside the Plan package. The report has two sections: `Business` (a
98
+ PASS, FAIL or BLOCKED conclusion with FINDs, RISKs and BLOCKs) and
99
+ `Evaluations` (an assessment of each role of the Implement run).
100
+ 4. **Fix.** When Business is FAIL, assign "Fix per report ...". Implement runs a
101
+ repair run with the team named in the report, then you ask for a new Review
102
+ round.
103
+ 5. **Commit.** The Agent commits or pushes only when you ask. Changes in the
104
+ target repository are committed in that repository; a Hub commit carries the
105
+ framework improvements of the same run. Plans, reports and records stay on
106
+ your machine.
107
+
108
+ ## Reading the self-improvement part after each run
109
+
110
+ At the end of each Plan, Implement or Review run, the Agent's response has:
111
+
112
+ - **Self-improvement:** the record ID, and for each applied improvement: the
113
+ issue, cause, file and section edited, old → new version. Look at the diff of
114
+ these files before committing.
115
+ - **Needs confirmation:** the closing block of the answer; it has a point about
116
+ this improvement only when the improvement would loosen a safety rule. You
117
+ choose to apply, skip, or have it re-evaluated by a separate agent; the
118
+ details of the issue and the change are in the "Self-improvement" section.
119
+ The evaluation is always delegated to a separate agent; only when the host
120
+ cannot create agents does the Agent evaluate itself and state the reason.
121
+
122
+ Safety rules (commit, push, secrets, destructive actions, points that need your
123
+ decision) are never loosened by self-improvement without your consent. The full
124
+ definition is in [`AGENTS.md`](AGENTS.md#safety-rules-and-self-improvement).
125
+
126
+ Records live in `.agents/evals/records/<layer>/<YYYY-MM-DD>.jsonl`. They
127
+ describe your projects, so Git ignores that directory: records stay on your
128
+ machine and the tool still reads them. To look up history:
129
+
130
+ ```powershell
131
+ python -B .agents/evals/layer_record.py query --issue-key <issue key>
132
+ python -B .agents/evals/layer_record.py query --layer review --plan-set <package> --task T01
133
+ python -B .agents/evals/layer_record.py check
134
+ ```
135
+
136
+ ## Checks after editing the framework
137
+
138
+ ```powershell
139
+ python -B -m unittest discover -s .agents/evals/tests -p "test_*.py"
140
+ ```
141
+
142
+ Pick the check commands that match what you edited: see
143
+ [Framework checks](.agents/instructions/standalone-and-maintenance.instructions.md#framework-checks).
144
+
145
+ ## Directory structure
146
+
147
+ ```text
148
+ AGENTS.md root: core rules, flow, choosing the kind of work, delegation, target repository
149
+ CONTEXT.md how responses to the user are presented
150
+ .agents/ layer instructions, roles, evals (validator, record tool, tests, local records)
151
+ .skills/ the pirl-plan, pirl-implement and pirl-review skills
152
+ .tmp/ temporary output, not in git
153
+ ```
154
+
155
+ Full tree: [framework structure](.agents/instructions/standalone-and-maintenance.instructions.md#framework-structure).
156
+
157
+ ## Reference documentation
158
+
159
+ | To learn about | Read |
160
+ | --- | --- |
161
+ | Core rules, flow, delegation, target repository | [`AGENTS.md`](AGENTS.md) |
162
+ | How the Agent presents answers, summaries, proposals | [`CONTEXT.md`](CONTEXT.md) |
163
+ | Choosing a layer, shared contracts, functional tests, self-improvement | [Router](.agents/instructions/layered-task-execution.instructions.md) |
164
+ | Each layer's decisions | [Plan](.agents/instructions/plan.instructions.md), [Implement](.agents/instructions/implement.instructions.md), [Review](.agents/instructions/review.instructions.md) |
165
+ | Running each layer, file formats, templates | [`pirl-plan`](.skills/pirl-plan/SKILL.md), [`pirl-implement`](.skills/pirl-implement/SKILL.md), [`pirl-review`](.skills/pirl-review/SKILL.md) |
166
+ | Each role's expertise | [`.agents/roles/`](.agents/roles/) |
167
+
168
+ ## License
169
+
170
+ [MIT](LICENSE).
@@ -0,0 +1,12 @@
1
+ .tmp/
2
+ __pycache__/
3
+ *.stackdump
4
+
5
+ # External local tools: kept on the local machine, not part of the framework.
6
+ .tools/
7
+
8
+ # Private data about the user's projects: kept on the local machine, never committed.
9
+ .agents/evals/records/
10
+ .repos/
11
+ # Plan packages <request-slug>_<YYYYMMDD-HHmmss>/ and the Review reports inside them, anywhere in the Hub.
12
+ *_[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]/
@@ -0,0 +1,45 @@
1
+ {
2
+ "version": "1.1.0",
3
+ "digest": "7826b2333a1d935560c750991fff036ffaa9b204",
4
+ "files": {
5
+ ".agents/evals/layer_record.py": "e9dc56cf38b3723aed3c3f22a2a8c0d0c3dfa32c",
6
+ ".agents/evals/tests/test_layer_record.py": "ed6bca11e9103c6be9a1436bc764a2fd14639e75",
7
+ ".agents/evals/tests/test_skills.py": "43ae9a91adc7a338f3609e32edffa8ae2d39ed3f",
8
+ ".agents/evals/tests/test_validate_layer_artifact.py": "013445c684ebc38dfbba919fb234a1a8984c1213",
9
+ ".agents/evals/validate_layer_artifact.py": "cef4fe2a85a983828f201a5ff9ce74e448a1bfc7",
10
+ ".agents/instructions/implement.instructions.md": "d168b1c2d0d229047561327091bc143a65924744",
11
+ ".agents/instructions/layered-task-execution.instructions.md": "bebc5f25f6b0b409a1054b43f8d8ede2f6df9b24",
12
+ ".agents/instructions/layout-verification.instructions.md": "746abd0793ec096c2d69501e00a16b3b6433aad4",
13
+ ".agents/instructions/plan.instructions.md": "59358acd397653f71a5f15bc1c2be0ccdf6045c9",
14
+ ".agents/instructions/review.instructions.md": "ba0fd8a558d5c44947189f17e4d430c1a3eb0982",
15
+ ".agents/instructions/self-evaluation.instructions.md": "280c5ec812fd61ed31809037dfcd2d593cc47c1a",
16
+ ".agents/instructions/standalone-and-maintenance.instructions.md": "15a96334813b1d74f824118c70a18f0b6712dbef",
17
+ ".agents/roles/backend-developer.role.md": "b41ae3aa5f930369f995293dabce797b58cc3688",
18
+ ".agents/roles/business-analyst.role.md": "6552002bff81ca4da70da9d05145c49154f23fcf",
19
+ ".agents/roles/code-analyst.role.md": "d171fb0388ef2b564d6994baaa0ca9b450686121",
20
+ ".agents/roles/code-reviewer.role.md": "8f7053eff48ac246cef36bd9fa6dab03eca88f4d",
21
+ ".agents/roles/frontend-developer.role.md": "5feffc5035281e787c2d1f6b4bf53adc017b5601",
22
+ ".agents/roles/process-auditor.role.md": "896f30f77a4d07d31258f13d65e81e867914a30e",
23
+ ".agents/roles/solution-architect.role.md": "48855ae414d3af4ca8bf42f99cbd8ad5d5e73da5",
24
+ ".agents/roles/technical-writer.role.md": "aff96083ffe43a11395b1277c208aa991b74bdd3",
25
+ ".agents/roles/ux-designer.role.md": "349268f8817a36facf4a854daaa571d3fb62022d",
26
+ ".gitignore": "9e417338f1bd7356a16e4e9de85ee6b3ceadc315",
27
+ ".skills/pirl-implement/SKILL.md": "da0931090bc11292fa41c0122c31cf4749d80d5c",
28
+ ".skills/pirl-implement/assets/implementation-record-template.md": "66bfc2333f1bcf7a8b4dd3394f2ebb8a960cfccd",
29
+ ".skills/pirl-implement/references/implementation-record-format.md": "bad5b435a8ac4ed28a9d9c933549cb413ad2bfdc",
30
+ ".skills/pirl-implement/scripts/add_implementation_record.py": "43a5dcdefd24c9dcac4930a26fb842dea1ed9ded",
31
+ ".skills/pirl-implement/scripts/new_layout_run.py": "fbfd0f44b09200b4d7718aed5953680cc7ea45a3",
32
+ ".skills/pirl-plan/SKILL.md": "27b4903bfb12418c465e0b45f9b70e740ae7b1e2",
33
+ ".skills/pirl-plan/assets/plan-template.md": "560bf8b8dca37d1b203b2c4cd643007c688dc10e",
34
+ ".skills/pirl-plan/references/plan-format.md": "53d518428920b332b6678310105d3c3a48979f0d",
35
+ ".skills/pirl-plan/scripts/new_plan.py": "ac949673d7c7a779200009dae2992d25381b8e5d",
36
+ ".skills/pirl-review/SKILL.md": "35c8308aa6e4c18047953ccad5ff41172461d943",
37
+ ".skills/pirl-review/assets/review-template.md": "1f27f7082d01c8eb4de94f9693913baf7aad64a2",
38
+ ".skills/pirl-review/references/review-format.md": "5a24c923a7d517bd4d1f42d0dd5907aac81a72a9",
39
+ ".skills/pirl-review/scripts/new_review.py": "27150e6a6e249507c839a05a7f337380a062b1c1",
40
+ "AGENTS.md": "ba49644c3d519843cd7dc97d6d8a52ca8bd5d23e",
41
+ "CONTEXT.md": "2779b5131d1ff7e8963c2516709d1a40c341823e",
42
+ "LICENSE": "f5a48e5836ce6a46ffcf13a8240a954b244b90ae",
43
+ "README.md": "d53ba7374742fae5e519bbc7953878ad858f7ca6"
44
+ }
45
+ }
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { join } from 'node:path';
3
+ import { render } from 'ink';
4
+ import { createClipboard } from '../core/clipboard.js';
5
+ import { loadConfig, saveConfig } from '../core/config.js';
6
+ import { exec } from '../core/exec.js';
7
+ import { configFile, displayPath, hubDir, packageInfo, packageRoot, pirlHome } from '../core/paths.js';
8
+ import { detectTargetRepo } from '../core/targetRepo.js';
9
+ import { runDoctor } from '../doctor/doctor.js';
10
+ import { readClaudeQuota } from '../engine/claudeQuota.js';
11
+ import { attachmentContext, pathIndex, resolveMentions } from '../engine/attachments.js';
12
+ import { createClaudeRunner } from '../engine/claudeRunner.js';
13
+ import { readPlanPackages, readRecords } from '../engine/hubData.js';
14
+ import { RunController } from '../engine/run.js';
15
+ import { listRuns, saveRun } from '../engine/runStore.js';
16
+ import { loadBundledFramework } from '../hub/framework.js';
17
+ import { frameworkServices } from '../hub/services.js';
18
+ import { ensureHub } from '../hub/hub.js';
19
+ import { getProvider } from '../providers/index.js';
20
+ import { App } from '../ui/App.js';
21
+ import { MOUSE_OFF, mouseSequence } from '../ui/terminalInput.js';
22
+ /** The run's model settings as the provider's arguments; `default` and `inherit` leave the choice to it. */
23
+ export const runModels = (config) => ({
24
+ model: config.model === 'default' ? undefined : config.model,
25
+ roleModel: config.roleModel === 'inherit' ? undefined : config.roleModel,
26
+ effort: config.effort === 'default' ? undefined : config.effort,
27
+ });
28
+ const HELP = `PIRL CLI: chạy PIRL framework (Plan – Implement – Review) bằng agent CLI bạn đã đăng nhập.
29
+
30
+ Cách dùng
31
+ pirl Mở TUI. Thư mục hiện tại (git repo) là target repo.
32
+ pirl --version In phiên bản CLI và framework đi kèm.
33
+ pirl --help In hướng dẫn này.
34
+
35
+ Trong TUI, gõ / để xem lệnh, hoặc /help để xem lệnh và phím tắt.`;
36
+ async function main() {
37
+ const root = packageRoot();
38
+ const pkg = packageInfo(root);
39
+ const args = process.argv.slice(2);
40
+ if (args.includes('--help') || args.includes('-h')) {
41
+ console.log(HELP);
42
+ return 0;
43
+ }
44
+ if (args.includes('--version') || args.includes('-v')) {
45
+ let framework = 'không có';
46
+ try {
47
+ framework = `fw-${loadBundledFramework(root).manifest.version}`;
48
+ }
49
+ catch { }
50
+ console.log(`pirl ${pkg.version} (framework ${framework})`);
51
+ return 0;
52
+ }
53
+ if (args.length > 0) {
54
+ console.error(`pirl: không nhận tham số "${args[0]}". Lệnh chạy bên trong TUI.\n\n${HELP}`);
55
+ return 2;
56
+ }
57
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
58
+ console.error('pirl cần chạy trong một terminal tương tác.');
59
+ return 1;
60
+ }
61
+ const home = pirlHome();
62
+ const notes = [];
63
+ const { config, warning } = loadConfig(configFile(home));
64
+ if (warning)
65
+ notes.push({ tone: 'warning', text: warning });
66
+ const target = await detectTargetRepo(process.cwd());
67
+ if (target.kind === 'none') {
68
+ notes.push({ tone: 'warning', text: `${target.reason}: các layer bị khoá.`, hint: 'Mở `pirl` trong thư mục của một git repo để dùng /pirl-plan, /pirl-implement, /pirl-review.' });
69
+ }
70
+ let bundle = null;
71
+ let hub = null;
72
+ let hubError = null;
73
+ try {
74
+ bundle = loadBundledFramework(root);
75
+ const result = await ensureHub(hubDir(home), bundle);
76
+ hub = result.status;
77
+ if (result.created)
78
+ notes.push({ tone: 'success', text: `Đã tạo Hub tại ${displayPath(hub.path)} với framework ${hub.upstreamTag ?? ''}.` });
79
+ }
80
+ catch (error) {
81
+ hubError = error instanceof Error ? error.message : String(error);
82
+ notes.push({ tone: 'error', text: `Không chuẩn bị được Hub: ${hubError}` });
83
+ }
84
+ const targetRoot = target.kind === 'git' ? target.root : null;
85
+ // `@` mentions are read from the target repository, or from the start directory outside one.
86
+ const attachBase = targetRoot ?? process.cwd();
87
+ const paths = cachedPaths(() => pathIndex(attachBase));
88
+ const runner = createClaudeRunner();
89
+ const instance = render(_jsx(App, { info: { cliVersion: pkg.version, target, hub, hubError }, initialConfig: config, startupNotes: notes, services: {
90
+ runDoctor: current => runDoctor({ exec, provider: getProvider(current.provider), target, hub, hubError }),
91
+ saveConfig: next => saveConfig(configFile(home), next),
92
+ startRun: options => new RunController({ runner, hub: hub.path, target: targetRoot, logDir: join(home, 'logs'), quota: () => readClaudeQuota('claude', hub.path), python: options.python, maxParallelRoles: options.config.maxParallelRoles, recentOutputs: options.recentOutputs, attachments: options.attachments, ...runModels(options.config) }, options.id, options.kind, options.request, options.resume),
93
+ saveRun: run => targetRoot && saveRun(home, targetRoot, run),
94
+ runs: () => (targetRoot ? listRuns(home, targetRoot) : []),
95
+ plans: () => (hub && targetRoot ? readPlanPackages(hub.path, targetRoot) : []),
96
+ records: () => (hub ? readRecords(hub.path, targetRoot) : []),
97
+ clipboard: createClipboard({ writeTerminal: data => process.stdout.write(data) }),
98
+ quota: () => (hub ? readClaudeQuota('claude', hub.path) : Promise.resolve(null)),
99
+ paths: () => paths.get(),
100
+ attach: text => attachmentContext(resolveMentions(text, attachBase, process.cwd()), [attachBase, ...(hub ? [hub.path] : [])]),
101
+ setMouse: mode => process.stdout.write(mouseSequence(mode)),
102
+ framework: hub && bundle ? frameworkServices(hub.path, bundle, runner, config => runModels(config).model) : undefined,
103
+ } }),
104
+ // Ctrl+C is handled by the TUI: it stops a running run first. The kitty keyboard protocol, where the
105
+ // terminal supports it (Windows Terminal 1.25+, kitty, WezTerm, Ghostty, iTerm2), tells Shift+Enter from Enter.
106
+ // Incremental rendering rewrites only the rows that changed: a spinner tick sends a few bytes, not a screen.
107
+ { alternateScreen: true, exitOnCtrlC: false, kittyKeyboard: { mode: 'auto' }, incrementalRendering: true });
108
+ // The terminal keeps reporting the mouse after the TUI ends unless it is told to stop, even on a crash.
109
+ process.once('exit', () => process.stdout.write(MOUSE_OFF));
110
+ await instance.waitUntilExit();
111
+ process.stdout.write(MOUSE_OFF);
112
+ return 0;
113
+ }
114
+ main().then(code => process.exit(code), error => {
115
+ console.error(error instanceof Error ? error.stack : error);
116
+ process.exit(1);
117
+ });
118
+ /** The path index for `@` completion, read again when older than a few seconds so new files show up. */
119
+ function cachedPaths(read, maxAgeMs = 10_000) {
120
+ let cached = null;
121
+ return {
122
+ get() {
123
+ if (!cached || Date.now() - cached.at > maxAgeMs)
124
+ cached = { at: Date.now(), value: read().catch(() => []) };
125
+ return cached.value;
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // React and Ink run their development builds unless NODE_ENV says otherwise; the development build checks
3
+ // every render and costs several times the CPU. Set it before they load, which is why the TUI is imported here.
4
+ process.env.NODE_ENV ??= 'production';
5
+ await import('./main.js');
6
+ export {};
@@ -0,0 +1,57 @@
1
+ import { commandUsage } from './registry.js';
2
+ const LAYERS = { 'pirl-plan': 'plan', 'pirl-implement': 'implement', 'pirl-review': 'review' };
3
+ const LABELS = { plan: 'Plan', implement: 'Implement', review: 'Review' };
4
+ export function dispatch(input, context) {
5
+ switch (input.kind) {
6
+ case 'empty':
7
+ return [];
8
+ case 'unknown-command':
9
+ return [{ type: 'note', tone: 'error', text: `Không có lệnh /${input.name}.`, hint: 'Gõ /help để xem danh sách lệnh.' }];
10
+ case 'text':
11
+ // The coordinator classifies free text (question, standalone task, framework maintenance) per AGENTS.md.
12
+ return [{ type: 'run', kind: 'chat', request: input.text }];
13
+ case 'command':
14
+ break;
15
+ }
16
+ const { spec, request } = input;
17
+ if (spec.name in LAYERS) {
18
+ const kind = LAYERS[spec.name];
19
+ if (context.target.kind !== 'git') {
20
+ return [
21
+ {
22
+ type: 'note',
23
+ tone: 'error',
24
+ text: `Layer ${LABELS[kind]} bị khoá: ${context.target.reason.toLowerCase()}.`,
25
+ hint: 'Thoát (/exit), chuyển tới thư mục của một git repo rồi gõ lại `pirl`.',
26
+ },
27
+ ];
28
+ }
29
+ if (!request)
30
+ return [{ type: 'note', tone: 'warning', text: 'Thiếu nội dung request.', hint: `Cú pháp: ${commandUsage(spec)}` }];
31
+ return [{ type: 'run', kind, request }];
32
+ }
33
+ switch (spec.name) {
34
+ case 'help':
35
+ return [{ type: 'overlay', overlay: 'help' }];
36
+ case 'doctor':
37
+ return [{ type: 'overlay', overlay: 'doctor' }, { type: 'doctor' }];
38
+ case 'settings':
39
+ return [{ type: 'overlay', overlay: 'settings' }];
40
+ case 'framework':
41
+ return [{ type: 'overlay', overlay: 'framework' }];
42
+ case 'plans':
43
+ return [{ type: 'overlay', overlay: 'plans' }];
44
+ case 'records':
45
+ return [{ type: 'overlay', overlay: 'records' }];
46
+ case 'resume':
47
+ return [{ type: 'resume' }];
48
+ case 'copy':
49
+ return [{ type: 'copy' }];
50
+ case 'clear':
51
+ return [{ type: 'clear' }];
52
+ case 'exit':
53
+ return [{ type: 'exit' }];
54
+ default:
55
+ return [];
56
+ }
57
+ }
@@ -0,0 +1,39 @@
1
+ export const COMMANDS = [
2
+ { name: 'pirl-plan', group: 'Layer', summary: 'Tạo hoặc điều chỉnh Plan', takesRequest: true },
3
+ { name: 'pirl-implement', group: 'Layer', summary: 'Thực hiện một task của Plan, hoặc sửa theo report', takesRequest: true },
4
+ { name: 'pirl-review', group: 'Layer', summary: 'Review kết quả implement của một task', takesRequest: true },
5
+ { name: 'resume', group: 'Run', summary: 'Chạy tiếp run bị dừng', takesRequest: false },
6
+ { name: 'plans', group: 'Tra cứu', summary: 'Danh sách Plan, task, report của repo này', takesRequest: false },
7
+ { name: 'records', group: 'Tra cứu', summary: 'Tra cứu record tự cải tiến', takesRequest: false },
8
+ { name: 'settings', group: 'Cấu hình', summary: 'Provider, giao diện, số role chạy song song', takesRequest: false },
9
+ { name: 'doctor', group: 'Cấu hình', summary: 'Kiểm tra provider, Python, Git, Hub', takesRequest: false },
10
+ { name: 'framework', group: 'Framework', summary: 'Phiên bản framework, cải tiến cục bộ, cập nhật', takesRequest: false },
11
+ { name: 'copy', group: 'Phiên', summary: 'Copy câu trả lời gần nhất của PIRL', takesRequest: false },
12
+ { name: 'clear', group: 'Phiên', summary: 'Xoá khung chat, bắt đầu hội thoại mới', takesRequest: false },
13
+ { name: 'help', group: 'Phiên', summary: 'Danh sách lệnh và phím tắt', takesRequest: false },
14
+ { name: 'exit', group: 'Phiên', summary: 'Thoát PIRL', takesRequest: false },
15
+ ];
16
+ export const commandUsage = (spec) => `/${spec.name}${spec.takesRequest ? ' <request>' : ''}`;
17
+ export function parseInput(raw) {
18
+ const text = raw.trim();
19
+ if (!text)
20
+ return { kind: 'empty' };
21
+ if (!text.startsWith('/'))
22
+ return { kind: 'text', text };
23
+ const match = /^\/(\S*)\s*([\s\S]*)$/.exec(text);
24
+ const name = match[1].toLowerCase();
25
+ const spec = COMMANDS.find(command => command.name === name);
26
+ return spec ? { kind: 'command', spec, request: match[2].trim() } : { kind: 'unknown-command', name };
27
+ }
28
+ /**
29
+ * Commands offered while the user types a command name: input starting with "/" and no space yet.
30
+ * Prefix matches come first, then commands that contain the typed text elsewhere (so "/plan" finds /pirl-plan).
31
+ */
32
+ export function suggestCommands(input) {
33
+ if (!input.startsWith('/') || /\s/.test(input))
34
+ return [];
35
+ const typed = input.slice(1).toLowerCase();
36
+ const prefix = COMMANDS.filter(command => command.name.startsWith(typed));
37
+ const inside = COMMANDS.filter(command => !command.name.startsWith(typed) && command.name.includes(typed));
38
+ return [...prefix, ...inside];
39
+ }
@@ -0,0 +1,70 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { exec as defaultExec, succeeded } from './exec.js';
5
+ const POWERSHELL_FLAGS = ['-NoProfile', '-NonInteractive', '-Command'];
6
+ function tools(platform, env) {
7
+ const windows = platform === 'win32' || Boolean(env['WSL_DISTRO_NAME']);
8
+ if (windows) {
9
+ const powershell = platform === 'win32' ? 'powershell' : 'powershell.exe';
10
+ return {
11
+ windows,
12
+ copy: [{ command: powershell, args: POWERSHELL_FLAGS }],
13
+ paste: [{ command: powershell, args: [...POWERSHELL_FLAGS, '[Console]::OutputEncoding=[Text.Encoding]::UTF8; Get-Clipboard -Raw'] }],
14
+ };
15
+ }
16
+ if (platform === 'darwin')
17
+ return { windows, copy: [{ command: 'pbcopy', args: [] }], paste: [{ command: 'pbpaste', args: [] }] };
18
+ const wayland = env['WAYLAND_DISPLAY'] ? [{ copy: { command: 'wl-copy', args: [] }, paste: { command: 'wl-paste', args: ['--no-newline'] } }] : [];
19
+ const x11 = [
20
+ { copy: { command: 'xclip', args: ['-selection', 'clipboard'] }, paste: { command: 'xclip', args: ['-selection', 'clipboard', '-o'] } },
21
+ { copy: { command: 'xsel', args: ['--clipboard', '--input'] }, paste: { command: 'xsel', args: ['--clipboard', '--output'] } },
22
+ ];
23
+ const all = [...wayland, ...x11];
24
+ return { windows, copy: all.map(tool => tool.copy), paste: all.map(tool => tool.paste) };
25
+ }
26
+ /** OSC 52 asks the terminal itself to set the clipboard; Windows Terminal, iTerm2, kitty and others honor it. */
27
+ export const osc52 = (text) => `\u001B]52;c;${Buffer.from(text, 'utf8').toString('base64')}\u0007`;
28
+ export function createClipboard(options = {}) {
29
+ const exec = options.exec ?? defaultExec;
30
+ const env = options.env ?? process.env;
31
+ const found = tools(options.platform ?? process.platform, env);
32
+ const utf8Env = { ...env, LANG: env['LANG'] || 'en_US.UTF-8', LC_CTYPE: env['LC_CTYPE'] || 'en_US.UTF-8' };
33
+ return {
34
+ async copy(text) {
35
+ for (const tool of found.copy) {
36
+ let result;
37
+ if (found.windows) {
38
+ const dir = mkdtempSync(join(tmpdir(), 'pirl-clip-'));
39
+ const file = join(dir, 'clip.txt');
40
+ try {
41
+ writeFileSync(file, text, 'utf8');
42
+ const quoted = file.replace(/'/g, "''");
43
+ result = await exec(tool.command, [...tool.args, `Get-Content -Raw -Encoding UTF8 -LiteralPath '${quoted}' | Set-Clipboard`], { timeoutMs: 15_000 });
44
+ }
45
+ finally {
46
+ rmSync(dir, { recursive: true, force: true });
47
+ }
48
+ }
49
+ else {
50
+ result = await exec(tool.command, tool.args, { input: text, env: utf8Env, timeoutMs: 15_000 });
51
+ }
52
+ if (succeeded(result))
53
+ return 'native';
54
+ }
55
+ if (options.writeTerminal) {
56
+ options.writeTerminal(osc52(text));
57
+ return 'terminal';
58
+ }
59
+ return 'failed';
60
+ },
61
+ async paste() {
62
+ for (const tool of found.paste) {
63
+ const result = await exec(tool.command, tool.args, { env: utf8Env, timeoutMs: 15_000 });
64
+ if (succeeded(result))
65
+ return found.windows ? result.stdout.replace(/\r\n/g, '\n').replace(/\n$/, '') : result.stdout;
66
+ }
67
+ return null;
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,73 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ export const PROVIDER_IDS = ['claude', 'codex', 'gemini', 'copilot'];
4
+ export const COLOR_SCHEME_IDS = ['pirl', 'ocean', 'forest', 'sunset', 'grape', 'mono'];
5
+ /** Model aliases the provider resolves to its newest version; `default` leaves the choice to the provider. */
6
+ export const MODEL_IDS = ['sonnet', 'opus', 'fable', 'haiku', 'default'];
7
+ /** `inherit` runs the roles on the coordinator's model. */
8
+ export const ROLE_MODEL_IDS = ['inherit', 'sonnet', 'opus', 'fable', 'haiku'];
9
+ /**
10
+ * What the mouse does in the TUI:
11
+ * - `select`: the terminal keeps the mouse, so dragging selects text as usual; the wheel reaches the TUI as the
12
+ * arrow keys (alternate scroll mode), which scroll the chat.
13
+ * - `wheel`: the TUI receives the wheel itself; selecting text then needs Shift held while dragging.
14
+ * - `off`: the TUI asks nothing of the mouse.
15
+ */
16
+ export const MOUSE_IDS = ['select', 'wheel', 'off'];
17
+ export const EFFORT_IDS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
18
+ export const DEFAULT_CONFIG = {
19
+ provider: 'claude',
20
+ theme: 'auto',
21
+ colors: 'pirl',
22
+ glyphs: 'auto',
23
+ motion: true,
24
+ mouse: 'select',
25
+ maxParallelRoles: 2,
26
+ model: 'sonnet',
27
+ roleModel: 'inherit',
28
+ effort: 'default',
29
+ };
30
+ export const PARALLEL_LIMITS = { min: 1, max: 4 };
31
+ const oneOf = (value, allowed, fallback) => typeof value === 'string' && allowed.includes(value) ? value : fallback;
32
+ /** Keeps every valid field of a stored config and falls back to the default for the rest. */
33
+ export function normalizeConfig(raw) {
34
+ const data = raw && typeof raw === 'object' ? raw : {};
35
+ const parallel = data['maxParallelRoles'];
36
+ return {
37
+ provider: oneOf(data['provider'], PROVIDER_IDS, DEFAULT_CONFIG.provider),
38
+ theme: oneOf(data['theme'], ['auto', 'dark', 'light'], DEFAULT_CONFIG.theme),
39
+ colors: oneOf(data['colors'], COLOR_SCHEME_IDS, DEFAULT_CONFIG.colors),
40
+ glyphs: oneOf(data['glyphs'], ['auto', 'unicode', 'ascii'], DEFAULT_CONFIG.glyphs),
41
+ motion: typeof data['motion'] === 'boolean' ? data['motion'] : DEFAULT_CONFIG.motion,
42
+ // Earlier versions stored on or off.
43
+ mouse: data['mouse'] === false ? 'off' : oneOf(data['mouse'], MOUSE_IDS, DEFAULT_CONFIG.mouse),
44
+ maxParallelRoles: typeof parallel === 'number' && Number.isInteger(parallel) && parallel >= PARALLEL_LIMITS.min && parallel <= PARALLEL_LIMITS.max
45
+ ? parallel
46
+ : DEFAULT_CONFIG.maxParallelRoles,
47
+ model: oneOf(data['model'], MODEL_IDS, DEFAULT_CONFIG.model),
48
+ roleModel: oneOf(data['roleModel'], ROLE_MODEL_IDS, DEFAULT_CONFIG.roleModel),
49
+ effort: oneOf(data['effort'], EFFORT_IDS, DEFAULT_CONFIG.effort),
50
+ };
51
+ }
52
+ export function loadConfig(file) {
53
+ let text;
54
+ try {
55
+ text = readFileSync(file, 'utf8');
56
+ }
57
+ catch {
58
+ return { config: { ...DEFAULT_CONFIG } };
59
+ }
60
+ try {
61
+ return { config: normalizeConfig(JSON.parse(text)) };
62
+ }
63
+ catch {
64
+ return { config: { ...DEFAULT_CONFIG }, warning: `Không đọc được ${file}; đang dùng cài đặt mặc định` };
65
+ }
66
+ }
67
+ /** Writes through a temporary file so an interrupted write never leaves a broken config. */
68
+ export function saveConfig(file, config) {
69
+ mkdirSync(dirname(file), { recursive: true });
70
+ const temporary = `${file}.tmp`;
71
+ writeFileSync(temporary, JSON.stringify(config, null, 2) + '\n', 'utf8');
72
+ renameSync(temporary, file);
73
+ }
@@ -0,0 +1,32 @@
1
+ import spawn from 'cross-spawn';
2
+ /** Runs a program without a shell. cross-spawn resolves `.cmd` shims on Windows (claude, npm, ...). */
3
+ export const exec = (command, args, options = {}) => new Promise(resolve => {
4
+ let stdout = '';
5
+ let stderr = '';
6
+ let timedOut = false;
7
+ let settled = false;
8
+ const finish = (result) => {
9
+ if (settled)
10
+ return;
11
+ settled = true;
12
+ clearTimeout(timer);
13
+ resolve(result);
14
+ };
15
+ const child = spawn(command, args, {
16
+ cwd: options.cwd,
17
+ env: options.env ?? process.env,
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ windowsHide: true,
20
+ });
21
+ const timer = setTimeout(() => {
22
+ timedOut = true;
23
+ child.kill();
24
+ }, options.timeoutMs ?? 30_000);
25
+ child.stdout?.setEncoding('utf8').on('data', (chunk) => (stdout += chunk));
26
+ child.stderr?.setEncoding('utf8').on('data', (chunk) => (stderr += chunk));
27
+ child.on('error', error => finish({ code: null, stdout, stderr, error: error.message }));
28
+ child.on('close', code => finish({ code: timedOut ? null : code, stdout, stderr, timedOut }));
29
+ child.stdin?.on('error', () => { });
30
+ child.stdin?.end(options.input ?? '');
31
+ });
32
+ export const succeeded = (result) => result.code === 0 && !result.error;