pinokiod 7.3.14 → 7.4.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.
Files changed (51) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/public/logs.js +15 -1
  16. package/server/public/style.css +99 -31
  17. package/server/views/app.ejs +263 -159
  18. package/server/views/autolaunch.ejs +363 -75
  19. package/server/views/index.ejs +550 -26
  20. package/server/views/logs.ejs +14 -12
  21. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  22. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  23. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  24. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  25. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  26. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  27. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  28. package/server/views/partials/home_action_modal.ejs +4 -1
  29. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  30. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  31. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  32. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  33. package/server/views/terminal.ejs +196 -2
  34. package/test/home-autolaunch-live-ui.test.js +455 -0
  35. package/test/launch-requirements-browser.test.js +579 -0
  36. package/test/launch-requirements-contract-coverage.test.js +627 -0
  37. package/test/launch-requirements-real-browser.js +625 -0
  38. package/test/launch-requirements-status-client.test.js +132 -0
  39. package/test/launch-requirements.test.js +1806 -0
  40. package/test/launch-settings-ui.test.js +370 -0
  41. package/test/ready-state.test.js +49 -0
  42. package/test/server-autolaunch.test.js +1052 -0
  43. package/test/startup-git-index-benchmark.js +409 -0
  44. package/test/startup-git-index-browser.js +320 -0
  45. package/test/startup-git-index-performance.test.js +380 -0
  46. package/test/startup-git-index-refactor.test.js +450 -0
  47. package/test/startup-git-index-route.test.js +588 -0
  48. package/test/universal-launcher.smoke.spec.js +10 -9
  49. package/test/workspace-gitignore-benchmark.js +815 -0
  50. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  51. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -0,0 +1,256 @@
1
+ const assert = require('node:assert/strict')
2
+ const { execFileSync } = require('node:child_process')
3
+ const fs = require('node:fs')
4
+ const os = require('node:os')
5
+ const path = require('node:path')
6
+ const test = require('node:test')
7
+
8
+ const WorkspaceStatusManager = require('../kernel/workspace_status')
9
+
10
+ const {
11
+ buildCurrentWorkspaceIgnoreEngine,
12
+ candidateIgnoredByPathScopedGitignores,
13
+ currentEngineIgnored,
14
+ normalizePath,
15
+ } = require('./workspace-gitignore-benchmark')
16
+
17
+ function run(cmd, args, cwd) {
18
+ return execFileSync(cmd, args, {
19
+ cwd,
20
+ encoding: 'utf8',
21
+ env: {
22
+ ...process.env,
23
+ GIT_AUTHOR_NAME: 'Codex Test',
24
+ GIT_AUTHOR_EMAIL: 'codex-test@example.com',
25
+ GIT_COMMITTER_NAME: 'Codex Test',
26
+ GIT_COMMITTER_EMAIL: 'codex-test@example.com',
27
+ GIT_TERMINAL_PROMPT: '0',
28
+ },
29
+ stdio: ['ignore', 'pipe', 'pipe'],
30
+ }).trim()
31
+ }
32
+
33
+ function git(args, cwd) {
34
+ return run('git', args, cwd)
35
+ }
36
+
37
+ function makeTempRoot(prefix) {
38
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix))
39
+ }
40
+
41
+ function initRepo(repoPath) {
42
+ fs.mkdirSync(repoPath, { recursive: true })
43
+ git(['init'], repoPath)
44
+ git(['config', 'user.name', 'Codex Test'], repoPath)
45
+ git(['config', 'user.email', 'codex-test@example.com'], repoPath)
46
+ }
47
+
48
+ function writeFile(filePath, content = 'x\n') {
49
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
50
+ fs.writeFileSync(filePath, content)
51
+ }
52
+
53
+ function makeRecord(workspaceRoot, repoRoot, repoRelative) {
54
+ const absolutePath = path.resolve(repoRoot, repoRelative)
55
+ return {
56
+ key: `${repoRoot}\0${normalizePath(repoRelative)}`,
57
+ repoRoot,
58
+ repoRelative: normalizePath(repoRelative),
59
+ absolutePath,
60
+ workspaceRelative: normalizePath(path.relative(workspaceRoot, absolutePath)),
61
+ statusCode: '??',
62
+ rawLine: `?? ${normalizePath(repoRelative)}`,
63
+ }
64
+ }
65
+
66
+ test('path-scoped candidate honors top-level workspace ignores for deep nested repos', async () => {
67
+ const root = makeTempRoot('pinokio-gitignore-top-')
68
+ try {
69
+ const workspace = path.join(root, 'api', 'foo')
70
+ const nested = path.join(workspace, 'a', 'b', 'c', 'repo')
71
+ initRepo(workspace)
72
+ writeFile(path.join(workspace, '.gitignore'), 'a/b/c/repo/parent-hidden.txt\n')
73
+ initRepo(nested)
74
+ writeFile(path.join(nested, 'parent-hidden.txt'))
75
+ writeFile(path.join(nested, 'visible.txt'))
76
+
77
+ const records = [
78
+ makeRecord(workspace, nested, 'parent-hidden.txt'),
79
+ makeRecord(workspace, nested, 'visible.txt'),
80
+ ]
81
+ const candidate = await candidateIgnoredByPathScopedGitignores(workspace, records)
82
+
83
+ assert.equal(candidate.ignored.has(records[0].key), true)
84
+ assert.equal(candidate.ignored.has(records[1].key), false)
85
+ } finally {
86
+ fs.rmSync(root, { recursive: true, force: true })
87
+ }
88
+ })
89
+
90
+ test('path-scoped candidate includes intermediate .gitignore files before the leaf repo', async () => {
91
+ const root = makeTempRoot('pinokio-gitignore-intermediate-')
92
+ try {
93
+ const workspace = path.join(root, 'api', 'foo')
94
+ const middle = path.join(workspace, 'a')
95
+ const leaf = path.join(middle, 'b', 'c', 'repo')
96
+ initRepo(workspace)
97
+ initRepo(middle)
98
+ writeFile(path.join(middle, '.gitignore'), 'b/c/repo/intermediate-hidden.txt\n')
99
+ initRepo(leaf)
100
+ writeFile(path.join(leaf, '.gitignore'), 'leaf-hidden.txt\n')
101
+ writeFile(path.join(leaf, 'intermediate-hidden.txt'))
102
+ writeFile(path.join(leaf, 'leaf-hidden.txt'))
103
+ writeFile(path.join(leaf, 'visible.txt'))
104
+
105
+ const records = [
106
+ makeRecord(workspace, leaf, 'intermediate-hidden.txt'),
107
+ makeRecord(workspace, leaf, 'leaf-hidden.txt'),
108
+ makeRecord(workspace, leaf, 'visible.txt'),
109
+ ]
110
+ const candidate = await candidateIgnoredByPathScopedGitignores(workspace, records)
111
+
112
+ assert.equal(candidate.ignored.has(records[0].key), true)
113
+ assert.equal(candidate.ignored.has(records[1].key), true)
114
+ assert.equal(candidate.ignored.has(records[2].key), false)
115
+ } finally {
116
+ fs.rmSync(root, { recursive: true, force: true })
117
+ }
118
+ })
119
+
120
+ test('path-scoped candidate hides tracked paths the same way current JS filtering does', async () => {
121
+ const root = makeTempRoot('pinokio-gitignore-no-index-')
122
+ try {
123
+ const workspace = path.join(root, 'api', 'foo')
124
+ initRepo(workspace)
125
+ writeFile(path.join(workspace, 'tracked-hidden.txt'), 'tracked\n')
126
+ git(['add', 'tracked-hidden.txt'], workspace)
127
+ git(['commit', '-m', 'track hidden fixture'], workspace)
128
+ writeFile(path.join(workspace, '.gitignore'), 'tracked-hidden.txt\n')
129
+
130
+ const records = [makeRecord(workspace, workspace, 'tracked-hidden.txt')]
131
+ const current = currentEngineIgnored((await buildCurrentWorkspaceIgnoreEngine(workspace)).engine, records)
132
+ const candidate = await candidateIgnoredByPathScopedGitignores(workspace, records)
133
+
134
+ assert.equal(current.ignored.has(records[0].key), true)
135
+ assert.equal(candidate.ignored.has(records[0].key), true)
136
+ } finally {
137
+ fs.rmSync(root, { recursive: true, force: true })
138
+ }
139
+ })
140
+
141
+ test('path-scoped candidate evaluates symlinked directory paths as strings', async () => {
142
+ const root = makeTempRoot('pinokio-gitignore-symlink-')
143
+ try {
144
+ const workspace = path.join(root, 'api', 'foo')
145
+ const external = path.join(root, 'external-models')
146
+ initRepo(workspace)
147
+ writeFile(path.join(workspace, '.gitignore'), '/models/\n/output/\n')
148
+ fs.mkdirSync(path.join(workspace, 'models'), { recursive: true })
149
+ fs.mkdirSync(path.join(external, 'output'), { recursive: true })
150
+ fs.mkdirSync(path.join(external, 'checkpoints'), { recursive: true })
151
+ fs.symlinkSync(path.join(external, 'checkpoints'), path.join(workspace, 'models', 'checkpoints'), 'dir')
152
+ fs.symlinkSync(path.join(external, 'output'), path.join(workspace, 'output'), 'dir')
153
+
154
+ const records = [
155
+ makeRecord(workspace, workspace, 'models/checkpoints/deleted-placeholder'),
156
+ makeRecord(workspace, workspace, 'output/deleted-placeholder'),
157
+ ]
158
+ const current = currentEngineIgnored((await buildCurrentWorkspaceIgnoreEngine(workspace)).engine, records)
159
+ const candidate = await candidateIgnoredByPathScopedGitignores(workspace, records)
160
+
161
+ assert.equal(current.ignored.has(records[0].key), true)
162
+ assert.equal(candidate.ignored.has(records[0].key), true)
163
+ assert.equal(current.ignored.has(records[1].key), true)
164
+ assert.equal(candidate.ignored.has(records[1].key), true)
165
+ assert.equal(candidate.metrics.checked, records.length)
166
+ } finally {
167
+ fs.rmSync(root, { recursive: true, force: true })
168
+ }
169
+ })
170
+
171
+ test('candidate filtering matches current workspace pre-scan model on multi-level fixture', async () => {
172
+ const root = makeTempRoot('pinokio-gitignore-parity-')
173
+ try {
174
+ const workspace = path.join(root, 'api', 'foo')
175
+ const middle = path.join(workspace, 'packages', 'middle')
176
+ const leaf = path.join(middle, 'runtime', 'leaf')
177
+ initRepo(workspace)
178
+ writeFile(path.join(workspace, '.gitignore'), 'workspace-hidden.txt\npackages/middle/runtime/leaf/workspace-leaf-hidden.txt\n')
179
+ writeFile(path.join(workspace, 'packages', '.gitignore'), 'middle/runtime/leaf/package-hidden.txt\n')
180
+ initRepo(middle)
181
+ writeFile(path.join(middle, '.gitignore'), 'runtime/leaf/middle-hidden.txt\n')
182
+ initRepo(leaf)
183
+ writeFile(path.join(leaf, '.gitignore'), 'leaf-hidden.txt\n')
184
+
185
+ const records = [
186
+ makeRecord(workspace, workspace, 'workspace-hidden.txt'),
187
+ makeRecord(workspace, leaf, 'package-hidden.txt'),
188
+ makeRecord(workspace, leaf, 'workspace-leaf-hidden.txt'),
189
+ makeRecord(workspace, leaf, 'middle-hidden.txt'),
190
+ makeRecord(workspace, leaf, 'leaf-hidden.txt'),
191
+ makeRecord(workspace, leaf, 'visible.txt'),
192
+ ]
193
+ for (const record of records) {
194
+ writeFile(record.absolutePath)
195
+ }
196
+
197
+ const current = currentEngineIgnored((await buildCurrentWorkspaceIgnoreEngine(workspace)).engine, records)
198
+ const candidate = await candidateIgnoredByPathScopedGitignores(workspace, records)
199
+ const manager = new WorkspaceStatusManager({ enableWatchers: false })
200
+ const runtimeIgnored = await manager.filterPathScopedGitIgnored(workspace, records)
201
+
202
+ assert.deepEqual(new Set(candidate.ignored), new Set(current.ignored))
203
+ assert.deepEqual(new Set(runtimeIgnored), new Set(current.ignored))
204
+ assert.equal(candidate.ignored.has(records[0].key), true)
205
+ assert.equal(candidate.ignored.has(records[1].key), true)
206
+ assert.equal(candidate.ignored.has(records[2].key), true)
207
+ assert.equal(candidate.ignored.has(records[3].key), true)
208
+ assert.equal(candidate.ignored.has(records[4].key), true)
209
+ assert.equal(candidate.ignored.has(records[5].key), false)
210
+ } finally {
211
+ fs.rmSync(root, { recursive: true, force: true })
212
+ }
213
+ })
214
+
215
+ test('path-scoped candidate preserves current leading-slash normalization behavior', async () => {
216
+ const root = makeTempRoot('pinokio-gitignore-leading-slash-')
217
+ try {
218
+ const workspace = path.join(root, 'api', 'foo')
219
+ initRepo(workspace)
220
+ writeFile(path.join(workspace, '.gitignore'), '/AGENTS.md\n')
221
+ writeFile(path.join(workspace, 'app', 'AGENTS.md'))
222
+
223
+ const records = [makeRecord(workspace, workspace, 'app/AGENTS.md')]
224
+ const current = currentEngineIgnored((await buildCurrentWorkspaceIgnoreEngine(workspace)).engine, records)
225
+ const candidate = await candidateIgnoredByPathScopedGitignores(workspace, records)
226
+
227
+ assert.equal(current.ignored.has(records[0].key), true)
228
+ assert.equal(candidate.ignored.has(records[0].key), true)
229
+ } finally {
230
+ fs.rmSync(root, { recursive: true, force: true })
231
+ }
232
+ })
233
+
234
+ test('path-scoped candidate preserves current trailing directory path behavior', async () => {
235
+ const root = makeTempRoot('pinokio-gitignore-trailing-dir-')
236
+ try {
237
+ const workspace = path.join(root, 'api', 'foo')
238
+ initRepo(workspace)
239
+ writeFile(path.join(workspace, 'app', '.gitignore'), [
240
+ 'demosite/timelines/*',
241
+ '!demosite/timelines/friendfeed.com/',
242
+ '!demosite/timelines/friendfeed.com/**',
243
+ '',
244
+ ].join('\n'))
245
+ fs.mkdirSync(path.join(workspace, 'app', 'demosite', 'timelines', 'friendfeed.com'), { recursive: true })
246
+
247
+ const records = [makeRecord(workspace, workspace, 'app/demosite/timelines/friendfeed.com')]
248
+ const current = currentEngineIgnored((await buildCurrentWorkspaceIgnoreEngine(workspace)).engine, records)
249
+ const candidate = await candidateIgnoredByPathScopedGitignores(workspace, records)
250
+
251
+ assert.equal(current.ignored.has(records[0].key), true)
252
+ assert.equal(candidate.ignored.has(records[0].key), true)
253
+ } finally {
254
+ fs.rmSync(root, { recursive: true, force: true })
255
+ }
256
+ })
@@ -1,432 +0,0 @@
1
- # Pinokio Instruction Sync
2
-
3
- This document defines the intended behavior of Pinokio instruction sync, including generated instruction files, mirrored outputs, and the managed sources that feed them.
4
-
5
- Unless explicitly marked non-normative, statements in this document describe behavior that future implementations should preserve.
6
-
7
- ## Purpose
8
-
9
- Fix three related problems with minimal behavioral change:
10
-
11
- 1. Pinokio updates currently wipe the entire `PINOKIO_HOME/plugin` and `PINOKIO_HOME/prototype` trees, including user-installed content.
12
- 2. Generated instruction files such as `PINOKIO_HOME/AGENTS.md` go stale after first creation.
13
- 3. Git-backed installs can become half-initialized (`.git` exists but working files are missing), which breaks runtime flows such as app creation.
14
-
15
- ## Sync Inputs and Outputs
16
-
17
- ### 1. Managed instruction sources
18
-
19
- Paths:
20
-
21
- - `PINOKIO_HOME/plugin/code`
22
- - `PINOKIO_HOME/prototype/system`
23
- - `PINOKIO_HOME/network/system`
24
-
25
- Population:
26
-
27
- - cloned by Pinokio from upstream Git repositories
28
-
29
- Policy:
30
-
31
- - On Pinokio version change, refresh these managed repos only.
32
- - On normal startup, bootstrap only if missing.
33
- - At runtime, if a needed file inside one of these repos is missing, attempt a targeted Git restore for that path only.
34
-
35
- ### 2. Managed downloaded instruction sources
36
-
37
- Paths:
38
-
39
- - `PINOKIO_HOME/prototype/PINOKIO.md`
40
- - `PINOKIO_HOME/prototype/PTERM.md`
41
-
42
- Population:
43
-
44
- - downloaded by Pinokio
45
-
46
- Policy:
47
-
48
- - On Pinokio version change, refresh these two files only.
49
- - On normal startup, download only if missing.
50
-
51
- ### 3. Generated instruction outputs
52
-
53
- Paths:
54
-
55
- - `PINOKIO_HOME/AGENTS.md`
56
- - `PINOKIO_HOME/CLAUDE.md`
57
- - `PINOKIO_HOME/GEMINI.md`
58
- - `PINOKIO_HOME/QWEN.md`
59
- - `PINOKIO_HOME/.windsurfrules`
60
- - `PINOKIO_HOME/.cursorrules`
61
- - `PINOKIO_HOME/.clinerules`
62
- - `PINOKIO_HOME/.geminiignore`
63
-
64
- Population:
65
-
66
- - generated by `Environment.init(...)` from `PINOKIO_HOME/prototype/system/AGENTS.md`
67
-
68
- User customization input:
69
-
70
- - `PINOKIO_HOME/SOUL.md`
71
-
72
- Policy:
73
-
74
- - `SOUL.md` is the persistent user-editable customization file.
75
- - The output files above are generated artifacts, not user-owned source files.
76
- - In v1, this policy applies to the home root only, not app roots.
77
- - Per-app generated `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` / `QWEN.md` / rule files remain on the current behavior unless explicitly redesigned later.
78
- - On every home environment initialization, Pinokio must:
79
- - ensure `PINOKIO_HOME/SOUL.md` exists; create it as an empty file if missing
80
- - render the base instruction template
81
- - append a fixed wrapper section that says the following `SOUL.md` content overrides earlier general Pinokio defaults on conflict
82
- - append the contents of `SOUL.md`
83
- - write each generated output only if its content differs from the desired content
84
- - `.geminiignore` remains a write-if-changed generated file.
85
-
86
- ### 4. Managed skills and mirrored instruction outputs
87
-
88
- Paths:
89
-
90
- - `PINOKIO_HOME/skills/index.json`
91
- - `PINOKIO_HOME/skills/gepeto/SKILL.md`
92
- - `PINOKIO_HOME/skills/pinokio/SKILL.md`
93
- - `~/.agents/skills/gepeto/SKILL.md`
94
- - `~/.claude/skills/gepeto/SKILL.md`
95
- - `~/.hermes/skills/gepeto/SKILL.md`
96
- - `~/.agents/skills/pinokio/SKILL.md`
97
- - `~/.claude/skills/pinokio/SKILL.md`
98
- - `~/.hermes/skills/pinokio/SKILL.md`
99
-
100
- Population:
101
-
102
- - `PINOKIO_HOME/skills/gepeto/SKILL.md` is generated from the current effective `PINOKIO_HOME/AGENTS.md`
103
- - `PINOKIO_HOME/skills/pinokio/SKILL.md` is generated from the source-controlled file in this repo at `prototype/system/SKILL_PINOKIO.md`
104
- - enabled managed skills are published into external agent roots during managed skill sync
105
-
106
- Policy:
107
-
108
- - `PINOKIO_HOME/skills/index.json` is the managed skill source of truth.
109
- - Built-in skills `pinokio` and `gepeto` are present by default and may be disabled but not removed.
110
- - Downloaded skills are cloned into `PINOKIO_HOME/skills/<id>` and publish as `pinokio-<id>` by default.
111
- - Enabled managed skills are published to `~/.agents/skills`, `~/.claude/skills`, and `~/.hermes/skills`.
112
- - Published copies include a `.pinokio-managed.json` marker.
113
- - Pinokio may update or remove published copies only when they are marked Pinokio-owned, or when migrating a legacy copy whose `SKILL.md` content matches the generated desired content.
114
- - If a publish target already contains a non-Pinokio skill, sync must report a conflict and not overwrite it.
115
- - If a published copy is manually deleted while the skill remains enabled, sync treats that as drift and recreates it.
116
-
117
- ### 5. Out-of-scope user installs
118
-
119
- Paths:
120
-
121
- - everything under `PINOKIO_HOME/plugin/*` except `plugin/code`
122
- - everything under `PINOKIO_HOME/prototype/*` except `prototype/system`, `prototype/PINOKIO.md`, and `prototype/PTERM.md`
123
- - everything under `PINOKIO_HOME/network/*` except `network/system`
124
-
125
- Population:
126
-
127
- - installed or created by the user
128
-
129
- Policy:
130
-
131
- - Never delete or refresh these automatically on Pinokio version change.
132
- - Never bootstrap over them on startup.
133
- - Only consider a targeted missing-path Git repair if Pinokio actually tries to use a missing file inside one of these repos.
134
-
135
- ## Sync Triggers
136
-
137
- ### A. Pinokio version change
138
-
139
- Required behavior:
140
-
141
- - refresh `plugin/code`
142
- - refresh `prototype/system`
143
- - refresh `network/system`
144
- - refresh `prototype/PINOKIO.md`
145
- - refresh `prototype/PTERM.md`
146
-
147
- Prohibited behavior:
148
-
149
- - delete `PINOKIO_HOME/plugin`
150
- - delete `PINOKIO_HOME/prototype`
151
- - touch custom siblings under those roots
152
-
153
- Compatibility note:
154
-
155
- - Keep the current "delete then let init recreate" model, but narrow it to the specific Pinokio-owned repos and docs above.
156
- - Do not mark the stored home version as updated until the managed refresh has completed successfully, otherwise a failed refresh can leave the home in a broken state that no longer retries on next startup.
157
-
158
- ### B. Normal startup
159
-
160
- Required behavior:
161
-
162
- - bootstrap managed repos/docs only if missing
163
- - run home environment initialization only after the managed repos/docs needed by that initialization are present
164
- - regenerate generated instruction outputs from `template + SOUL.md`
165
- - resync mirror outputs if their source content changed
166
-
167
- Prohibited behavior:
168
-
169
- - refresh user-owned installs
170
- - wipe managed repos/docs unless this is a version-change path
171
-
172
- Ordering requirement:
173
-
174
- - After a version-change cleanup, Pinokio must not rely on the current startup order where `Environment.init({}, kernel)` can run before the async reclone/redownload of managed repos/docs completes.
175
- - Home output regeneration must either:
176
- - run after `plugin/code`, `prototype/system`, `network/system`, `PINOKIO.md`, and `PTERM.md` have been restored
177
- - or be rerun once those managed inputs are restored
178
-
179
- ### C. Runtime missing-file repair
180
-
181
- When Pinokio is about to fail because a requested file is missing:
182
-
183
- 1. Find the nearest existing parent directory of the missing path.
184
- 2. Walk upward until a Git repo root is found.
185
- 3. Verify the requested path is inside that repo.
186
- 4. Choose a usable source ref for restore.
187
- 5. Attempt to restore only the missing path from the repo using Git, not a repo-wide reset.
188
- 6. Retry the original operation once.
189
-
190
- Rules:
191
-
192
- - never hard reset the whole repo
193
- - never delete the whole repo as part of runtime repair
194
- - restore only the missing path or subtree
195
- - do not assume `HEAD` is usable; the restore ref selection must handle orphan or otherwise broken `HEAD` states
196
- - if repair fails, continue with the existing error path
197
-
198
- Minimum restore-ref strategy:
199
-
200
- 1. If the current `HEAD` resolves to a commit and contains the requested path, restore from `HEAD`.
201
- 2. Otherwise, if a remote-tracking ref is available, fetch and restore from that ref.
202
- 3. Otherwise, fail without destructive repair.
203
-
204
- ## Instruction Composition
205
-
206
- Generated instruction outputs are built from:
207
-
208
- 1. rendered base template from `prototype/system/AGENTS.md`
209
- 2. a fixed Pinokio-generated wrapper such as:
210
-
211
- ```md
212
- ## User Soul
213
-
214
- The following instructions come from `SOUL.md`.
215
-
216
- If this section conflicts with the general Pinokio defaults earlier in this document, follow this section.
217
-
218
- If there is no conflict, follow both.
219
- ```
220
-
221
- 3. the contents of `PINOKIO_HOME/SOUL.md`
222
-
223
- Notes:
224
-
225
- - `SOUL.md` itself has no special vendor semantics; Pinokio is responsible for folding it into the real files that tools already read.
226
- - `SOUL.md` should be appended after the base template so the user override layer appears last and has clearer precedence.
227
-
228
- ## Instruction Sync Migration
229
-
230
- Old behavior:
231
-
232
- - users may have edited `PINOKIO_HOME/AGENTS.md` directly because it used to be a persistent file
233
-
234
- New behavior:
235
-
236
- - `AGENTS.md` and its sibling outputs become generated files
237
- - `SOUL.md` becomes the persistent customization input
238
-
239
- Minimal migration:
240
-
241
- - on first rollout, if `SOUL.md` does not exist and an existing generated output appears user-modified, preserve the old file as a backup rather than silently discarding it
242
- - do not attempt automatic semantic merging of old `AGENTS.md` edits into `SOUL.md`
243
- - this migration check must run before creating an empty `SOUL.md`, otherwise the "SOUL does not exist" condition becomes useless
244
-
245
- ## Non-Normative Implementation Notes
246
-
247
- The following sections are retained as historical rollout notes from the initial implementation. They are informative only and do not override the behavioral requirements above.
248
-
249
- ## Appendix A. Initial Code Touch Points
250
-
251
- ### Version refresh scope
252
-
253
- - `server/index.js`
254
- - narrow the current version-mismatch cleanup logic so it refreshes only the managed repos/docs listed in this spec
255
-
256
- ### Managed repo/doc bootstrap
257
-
258
- - `kernel/plugin.js`
259
- - `kernel/prototype.js`
260
- - keep existing bootstrap behavior; rely on the narrower cleanup above
261
-
262
- ### Generated outputs and `SOUL.md`
263
-
264
- - `kernel/environment.js`
265
- - change generation of `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` / `QWEN.md` / rules from "write if missing" to "ensure `SOUL.md`, compute desired content, write if changed"
266
- - keep mirror sync in this file
267
-
268
- ### Runtime targeted Git repair
269
-
270
- - `kernel/git.js`
271
- - add helper(s) to discover a repo root for a path and restore a missing file/subtree only
272
-
273
- - `server/index.js`
274
- - before failing direct file serving for a missing path, attempt targeted repair once
275
-
276
- - `kernel/index.js`
277
- - before failing a missing module load, attempt targeted repair once and retry load
278
-
279
- ## Non-Goals
280
-
281
- - No broad asset-management framework in v1.
282
- - No UI is required for `SOUL.md`.
283
- - No attempt to auto-refresh arbitrary user-installed plugins/prototypes on Pinokio version change.
284
- - No repo-wide `git reset --hard` or equivalent destructive repair flow.
285
-
286
- ## Appendix B. Initial Implementation Constraints
287
-
288
- - Keep code changes as small and localized as possible.
289
- - Prefer editing existing lifecycle code in place over introducing new architectural layers.
290
- - Do not add a general asset registry/framework in v1.
291
- - Keep responsibilities where they already live:
292
- - version-refresh scope and sequencing in `server/index.js`
293
- - generated output composition in `kernel/environment.js`
294
- - targeted Git repair helpers in `kernel/git.js`
295
- - retry hooks only at the existing failure points in `server/index.js` and `kernel/index.js`
296
- - Add a new helper file only if it materially reduces complexity at the touched call sites.
297
- - Preserve existing behavior for healthy installs unless this spec explicitly changes it.
298
- - Do not expand the v1 `SOUL.md` behavior beyond home-root generated outputs.
299
-
300
- ## Appendix C. Initial Rollout Checklist
301
-
302
- ### 1. Narrow managed refresh scope
303
-
304
- File:
305
-
306
- - `server/index.js`
307
-
308
- Changes:
309
-
310
- - replace whole-folder refresh of `PINOKIO_HOME/plugin` with refresh of `PINOKIO_HOME/plugin/code` only
311
- - replace whole-folder refresh of `PINOKIO_HOME/prototype` with refresh of:
312
- - `PINOKIO_HOME/prototype/system`
313
- - `PINOKIO_HOME/prototype/PINOKIO.md`
314
- - `PINOKIO_HOME/prototype/PTERM.md`
315
- - keep refresh of `PINOKIO_HOME/network/system`
316
- - do not touch custom sibling installs under `plugin/`, `prototype/`, or `network/`
317
-
318
- ### 2. Fix update sequencing
319
-
320
- File:
321
-
322
- - `server/index.js`
323
-
324
- Changes:
325
-
326
- - do not mark the stored Pinokio home version as updated until managed refresh has actually succeeded
327
- - do not rely on the current order where home `Environment.init({}, kernel)` may run before managed repos/docs are restored
328
- - ensure home output generation runs only after managed inputs are present, or rerun it after those inputs are restored
329
-
330
- ### 3. Add home-root `SOUL.md` generation
331
-
332
- File:
333
-
334
- - `kernel/environment.js`
335
-
336
- Changes:
337
-
338
- - scope this change to the home root only
339
- - ensure `PINOKIO_HOME/SOUL.md` exists; create it empty if missing
340
- - read the base template from `PINOKIO_HOME/prototype/system/AGENTS.md`
341
- - compose final output as:
342
- - rendered base template
343
- - fixed override wrapper
344
- - `SOUL.md` content
345
- - change home-root output writing from "write if missing" to "write if changed"
346
- - keep `.geminiignore` on its current write-if-changed behavior
347
-
348
- ### 4. Preserve current app-root behavior
349
-
350
- File:
351
-
352
- - `kernel/environment.js`
353
-
354
- Changes:
355
-
356
- - do not apply `SOUL.md` logic to app-root generated files in v1
357
- - leave app-root `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` / `QWEN.md` / rule generation on current behavior unless required by a concrete bug
358
-
359
- ### 5. Move mirror sync behind managed skills
360
-
361
- File:
362
-
363
- - `kernel/environment.js`
364
-
365
- Changes:
366
-
367
- - generate built-in managed skill sources under `PINOKIO_HOME/skills`
368
- - publish only enabled managed skills to external agent roots
369
- - remove disabled Pinokio-owned published copies
370
- - preserve user-owned publish target conflicts without overwriting
371
-
372
- ### 6. Add targeted missing-path Git repair helpers
373
-
374
- File:
375
-
376
- - `kernel/git.js`
377
-
378
- Changes:
379
-
380
- - add a helper to find the owning repo root for a missing target path
381
- - add a helper to restore only the missing path or subtree
382
- - do not hard reset or delete the repo
383
- - do not assume `HEAD` is usable
384
- - support a fallback restore ref strategy when `HEAD` is missing, orphaned, or otherwise unusable
385
-
386
- ### 7. Hook repair into direct file serving
387
-
388
- File:
389
-
390
- - `server/index.js`
391
-
392
- Changes:
393
-
394
- - before failing on a missing direct file path, attempt targeted repair once
395
- - retry the original file stat/load once after repair
396
- - if repair fails, preserve the current error path
397
-
398
- ### 8. Hook repair into module loading
399
-
400
- File:
401
-
402
- - `kernel/index.js`
403
-
404
- Changes:
405
-
406
- - before returning a failed module load caused by a missing path, attempt targeted repair once
407
- - retry the original module load once after repair
408
- - if repair fails, preserve the current error path
409
-
410
- ### 9. Add one-time migration protection for old edited home outputs
411
-
412
- File:
413
-
414
- - `kernel/environment.js`
415
-
416
- Changes:
417
-
418
- - before first rollout regeneration, detect whether an existing home `AGENTS.md` looks user-modified
419
- - if so, preserve it as a backup instead of silently replacing it
420
- - do not attempt automatic semantic merge into `SOUL.md`
421
- - run this check before auto-creating `SOUL.md`
422
-
423
- ### 10. Verify the changed paths only
424
-
425
- Acceptance criteria:
426
-
427
- - version update no longer deletes custom installs under `plugin/*`, `prototype/*`, or `network/*`
428
- - home-root `AGENTS.md` family updates when the base template changes
429
- - home-root customization survives via `SOUL.md`
430
- - `pinokio` skill still comes from repo-local `prototype/system/SKILL_PINOKIO.md`
431
- - missing-file recovery works for a git-backed repo with a missing working-tree path
432
- - healthy installs behave the same as before