@passioncode-ai/passioncode 0.1.27 → 0.1.29
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/CHANGELOG.md +49 -0
- package/COMMERCIAL-LICENSE.md +1 -1
- package/README.md +3 -2
- package/SECURITY.md +3 -1
- package/family.json +1 -1
- package/lib/launcher.js +110 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/observatory-log/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/observatory-log/hooks/session-start.sh +77 -4
- package/payload/plugins/observatory-log/skills/explaining-changes/SKILL.md +1 -1
- package/payload/plugins/observatory-log/skills/handling-secrets/SKILL.md +12 -2
- package/payload/plugins/observatory-log/skills/tracking-resources/SKILL.md +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/passioncode/hooks/probe.js +6 -2
- package/payload/plugins/passioncode/hooks/session-start.js +4 -3
- package/payload/plugins/passioncode/hooks/update-check.js +33 -1
- package/payload/plugins/passioncode/skills/working-in-passioncode/SKILL.md +6 -4
- package/payload/plugins/passioncode/skills/working-in-passioncode/references/publishing.md +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.29 - 2026-10-05
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Observatory Log is pinned at `v0.17.3`** (plugin 0.17.3; it was `v0.16.0`). Its
|
|
8
|
+
SessionStart hook keeps Project Observatory current:
|
|
9
|
+
- for an engine at 0.17.0 or later, it schedules the engine's hourly maintenance job where
|
|
10
|
+
it is missing;
|
|
11
|
+
- for an engine from 0.7.0 to 0.16.0, which has no updater of its own, it installs the newer
|
|
12
|
+
release once a day with that engine's `full update --apply`, unless the person turned
|
|
13
|
+
automatic updates off.
|
|
14
|
+
|
|
15
|
+
Both run detached, at most every six hours or once a day, and never hold up a session.
|
|
16
|
+
|
|
17
|
+
## 0.1.28 - 2026-10-05
|
|
18
|
+
|
|
19
|
+
The launcher meets the organization's
|
|
20
|
+
[product lifecycle contract](https://github.com/passioncode-ai/fabric-workspace/blob/main/knowledge/lifecycle.md):
|
|
21
|
+
findings F13, F14, F15 and F17 of the 2026-10-03 lifecycle audit. Observatory Log moves to
|
|
22
|
+
Project Observatory 0.16.0.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **Observatory Log is pinned at `v0.16.0`** (plugin 0.16.0; it was `v0.15.0`, plugin 0.15.0),
|
|
27
|
+
the Project Observatory 0.16.0 skills. The skills' text is unchanged and follows the release:
|
|
28
|
+
agent memory now checks an access binding on every call, embeds remotely only with the
|
|
29
|
+
operator's per-project consent, and can be erased with a receipt (`full forget`).
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **One update at a time (LC-03, F14).** `update`, `restore` and `uninstall` take
|
|
34
|
+
`~/.passioncode/update.lock`; a second one refuses with exit 1 and names the holder, so a
|
|
35
|
+
background update and a manual one no longer interleave their `claude plugin` writes.
|
|
36
|
+
- **One `npm view` for sessions started together (F13).** The SessionStart hook claims
|
|
37
|
+
`~/.passioncode/probe.pending` before it starts the probe; the probe removes it after recording,
|
|
38
|
+
and a marker older than five minutes is taken over.
|
|
39
|
+
- **Releases no longer accumulate (LC-11, LC-15, F15).** An update keeps the release it installed
|
|
40
|
+
and the one it replaced, and removes older releases and partial copies left by a killed update
|
|
41
|
+
(`prune` step; a dry run plans it).
|
|
42
|
+
- **A release proves its provenance (F17).** `vendor --release` refuses a member whose tag does not
|
|
43
|
+
name the plugin version it carries. A member whose repository's tags version another product
|
|
44
|
+
may declare `refVersions: "repository"` with the plugin `version` it expects; the bytes at that
|
|
45
|
+
tag must carry that version, and the manifest records `refVersions` so the pair reads as the
|
|
46
|
+
repository's release, not the plugin's. No member declares it yet: at the current pins every tag
|
|
47
|
+
names its plugin version (Observatory Log `v0.16.0` carries 0.16.0), and whether Observatory Log
|
|
48
|
+
should use it when the two diverge again is the open operator decision PC-04.
|
|
49
|
+
- `AGENTS.md` gains a `## Lifecycle` section: the background footprint with the SessionStart
|
|
50
|
+
hook's cost and cadence (LC-09) and build retention (LC-15).
|
|
51
|
+
|
|
3
52
|
## 0.1.27 - 2026-10-04
|
|
4
53
|
|
|
5
54
|
### Changed
|
package/COMMERCIAL-LICENSE.md
CHANGED
|
@@ -5,7 +5,7 @@ This repository is open source under the [GNU Affero General Public License v3.0
|
|
|
5
5
|
meet the AGPL's terms — for example inside a closed-source product, or as a modified hosted
|
|
6
6
|
service whose source is not published.
|
|
7
7
|
|
|
8
|
-
To obtain a commercial license, write to **
|
|
8
|
+
To obtain a commercial license, tell us about your use at **https://passioncode.ai/business/**, or write to **commercial@passioncode.ai**.
|
|
9
9
|
|
|
10
10
|
SPDX: `AGPL-3.0-only OR LicenseRef-PassionCode-Commercial`. Copyright (c) 2026 Siarhei Sheleh.
|
|
11
11
|
Contributions are accepted under [CLA.md](CLA.md), which allows this dual licence.
|
package/README.md
CHANGED
|
@@ -53,7 +53,8 @@ out of it, even when they implement Fabric protocols.
|
|
|
53
53
|
- Updates create no skill copies in `~/.claude/skills`: a plain copy there shadows the plugin. Such
|
|
54
54
|
copies, and hub entries that pointed at a repository's working tree, are moved into
|
|
55
55
|
`~/.passioncode/quarantine/` — `npx @passioncode-ai/passioncode@latest restore` puts them back.
|
|
56
|
-
- Each version is an immutable release under `~/.passioncode/releases/<version
|
|
56
|
+
- Each version is an immutable release under `~/.passioncode/releases/<version>`; an update
|
|
57
|
+
keeps the release it replaced for rollback and removes older ones.
|
|
57
58
|
|
|
58
59
|
## Staying current
|
|
59
60
|
|
|
@@ -114,7 +115,7 @@ reads a member from `<dir>/<repository name>`, and `PASSIONCODE_CHECKOUT_<MEMBER
|
|
|
114
115
|
## License
|
|
115
116
|
|
|
116
117
|
Open source under the [GNU AGPL-3.0](LICENSE). A [commercial license](COMMERCIAL-LICENSE.md) is
|
|
117
|
-
available for use that does not meet the AGPL's terms —
|
|
118
|
+
available for use that does not meet the AGPL's terms — [passioncode.ai/business](https://passioncode.ai/business/).
|
|
118
119
|
Versions before 0.1.12 were released under PolyForm Noncommercial or Internal Use (v0.1.5 to
|
|
119
120
|
v0.1.11) and MIT (v0.1.4 and earlier); each keeps the licence it was released under.
|
|
120
121
|
Contributions are accepted under [CLA.md](CLA.md) ([CONTRIBUTING.md](CONTRIBUTING.md)).
|
package/SECURITY.md
CHANGED
|
@@ -16,7 +16,9 @@ deleted: a recovery record is written before the move in
|
|
|
16
16
|
`~/.passioncode/quarantine/<run>/moved.json`. `restore` processes the latest batch,
|
|
17
17
|
checkpoints completed entries and refuses to overwrite a conflicting user replacement.
|
|
18
18
|
`uninstall` removes owned links/plugins, clears the active release and disables
|
|
19
|
-
automatic updates; it retains
|
|
19
|
+
automatic updates; it retains the current and previous release copies and recovery
|
|
20
|
+
records. Each update removes the launcher's own older releases (its output, reinstallable
|
|
21
|
+
from npm) and keeps the one it replaced for rollback. A failed removal
|
|
20
22
|
returns a nonzero exit code. See the [CLI recovery contract](docs/reference/cli.md).
|
|
21
23
|
|
|
22
24
|
## The self-update and who it trusts
|
package/family.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"name": "observatory-log",
|
|
23
23
|
"displayName": "Observatory Log",
|
|
24
24
|
"repo": "passioncode-ai/project-observatory-dashboard",
|
|
25
|
-
"ref": "v0.
|
|
25
|
+
"ref": "v0.17.3",
|
|
26
26
|
"kind": "plugin",
|
|
27
27
|
"path": "observatory/engine/skill/plugins/observatory-log",
|
|
28
28
|
"legacyPluginIds": [
|
package/lib/launcher.js
CHANGED
|
@@ -67,6 +67,94 @@ function symlinkAtomic(target, at) {
|
|
|
67
67
|
fs.renameSync(tmp, at);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// --- one update at a time (lifecycle contract LC-03) ---------------------------------------
|
|
71
|
+
// update, restore and uninstall all write installed_plugins.json, the hub and state.json. A
|
|
72
|
+
// background update (started by the session hook) and a manual one used to interleave their
|
|
73
|
+
// `claude plugin` writes. They now take one lock; the second refuses and names the holder.
|
|
74
|
+
const O_EXLOCK = 0x20; // BSD/macOS open(2) flag: take flock(LOCK_EX); absent from fs.constants
|
|
75
|
+
|
|
76
|
+
class UpdateRunning extends Error {
|
|
77
|
+
constructor(pid, file) {
|
|
78
|
+
super(`Another PassionCode update is already running (${pid ? `process ${pid}` : 'another process'} holds ${file}); let it finish, then retry.`);
|
|
79
|
+
this.pid = pid;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readPid(file) {
|
|
84
|
+
try { const text = fs.readFileSync(file, 'utf8').trim(); return /^\d{1,10}$/.test(text) ? Number(text) : null; } catch (_) { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function alive(pid) {
|
|
88
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Take ~/.passioncode/update.lock. On macOS it is a kernel flock (released when the holder dies,
|
|
93
|
+
* so it is never stale); elsewhere an exclusively created pid file whose holder must be alive.
|
|
94
|
+
* Returns `{ release() }`; throws UpdateRunning when another process holds it.
|
|
95
|
+
*/
|
|
96
|
+
function acquireUpdateLock(p) {
|
|
97
|
+
fs.mkdirSync(p.state, { recursive: true });
|
|
98
|
+
const file = path.join(p.state, 'update.lock');
|
|
99
|
+
if (process.platform === 'darwin') {
|
|
100
|
+
let fd;
|
|
101
|
+
try {
|
|
102
|
+
fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_CREAT | O_EXLOCK | fs.constants.O_NONBLOCK, 0o600);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error.code === 'EAGAIN' || error.code === 'EWOULDBLOCK') throw new UpdateRunning(readPid(file), file);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
fs.ftruncateSync(fd, 0);
|
|
108
|
+
fs.writeSync(fd, String(process.pid));
|
|
109
|
+
return { release() { if (fd !== null) { fs.closeSync(fd); fd = null; } } };
|
|
110
|
+
}
|
|
111
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
112
|
+
try {
|
|
113
|
+
fs.writeFileSync(file, String(process.pid), { flag: 'wx', mode: 0o600 });
|
|
114
|
+
return { release() { if (readPid(file) === process.pid) fs.rmSync(file, { force: true }); } };
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code !== 'EEXIST') throw error;
|
|
117
|
+
const pid = readPid(file);
|
|
118
|
+
if (pid && pid !== process.pid && alive(pid)) throw new UpdateRunning(pid, file);
|
|
119
|
+
// The holder is gone: move its file aside atomically, so two takers cannot both steal it.
|
|
120
|
+
try { fs.renameSync(file, `${file}.stale.${process.pid}`); fs.rmSync(`${file}.stale.${process.pid}`, { force: true }); } catch (_) { /* another taker moved it */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
throw new UpdateRunning(readPid(file), file);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function withUpdateLock(home, fn) {
|
|
127
|
+
const lock = acquireUpdateLock(paths(home));
|
|
128
|
+
try { return fn(); } finally { lock.release(); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- release retention (LC-11, LC-15) -----------------------------------------------------------
|
|
132
|
+
const semver = (v) => v.split('.').map(Number);
|
|
133
|
+
function newerVersion(a, b) {
|
|
134
|
+
const pa = semver(a), pb = semver(b);
|
|
135
|
+
for (let i = 0; i < 3; i += 1) if (pa[i] !== pb[i]) return pa[i] > pb[i];
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The release directories an update may remove: every one except the release it installed and
|
|
141
|
+
* the one it replaced (the rollback; the newest other release when it replaced none), plus any
|
|
142
|
+
* `*.partial` copy a killed update left behind (safe: the caller holds the update lock).
|
|
143
|
+
* Files, symlinks and anything not named like a release are never touched.
|
|
144
|
+
*/
|
|
145
|
+
function staleReleases(p, version, previous) {
|
|
146
|
+
let entries;
|
|
147
|
+
try { entries = fs.readdirSync(p.releases, { withFileTypes: true }); } catch (_) { return []; }
|
|
148
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.isSymbolicLink()).map((e) => e.name);
|
|
149
|
+
const partial = dirs.filter((n) => /^\d+\.\d+\.\d+\.\d+\.partial$/.test(n));
|
|
150
|
+
const versions = dirs.filter((n) => /^\d+\.\d+\.\d+$/.test(n));
|
|
151
|
+
let keep = previous && versions.includes(previous) && previous !== version ? previous : null;
|
|
152
|
+
if (!keep) {
|
|
153
|
+
for (const v of versions) if (v !== version && (!keep || newerVersion(v, keep))) keep = v;
|
|
154
|
+
}
|
|
155
|
+
return [...versions.filter((v) => v !== version && v !== keep), ...partial].map((n) => path.join(p.releases, n));
|
|
156
|
+
}
|
|
157
|
+
|
|
70
158
|
/** Claude Code's plugin CLI, overridable for tests. */
|
|
71
159
|
function claudeRunner() {
|
|
72
160
|
const bin = process.env.PASSIONCODE_CLAUDE || 'claude';
|
|
@@ -303,6 +391,16 @@ function update({ home, dryRun = false, claude = claudeRunner(), log = () => {}
|
|
|
303
391
|
}
|
|
304
392
|
}
|
|
305
393
|
|
|
394
|
+
// 5. The launcher's own releases: keep this one and the one it replaced, remove the rest.
|
|
395
|
+
// They are its own output, reinstallable from npm — not the person's files, which are
|
|
396
|
+
// only ever moved to the quarantine.
|
|
397
|
+
const stale = staleReleases(p, version, state.installed || null);
|
|
398
|
+
if (stale.length) {
|
|
399
|
+
step('prune', `remove ${stale.map((d) => path.basename(d)).join(', ')} from ${p.releases}; keep ${version} and the release before it`, () => {
|
|
400
|
+
for (const dir of stale) fs.rmSync(dir, { recursive: true, force: true });
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
306
404
|
if (!dryRun) {
|
|
307
405
|
if (moved.length) writeJson(path.join(quarantineDir, 'moved.json'), moved);
|
|
308
406
|
// A probe or config command may have written while the plugin CLI ran.
|
|
@@ -472,4 +570,15 @@ function uninstall({ home, claude = claudeRunner() } = {}) {
|
|
|
472
570
|
return removed;
|
|
473
571
|
}
|
|
474
572
|
|
|
475
|
-
module.exports = {
|
|
573
|
+
module.exports = {
|
|
574
|
+
// An invalid payload is refused before the lock creates anything under ~/.passioncode.
|
|
575
|
+
update: (opts = {}) => {
|
|
576
|
+
if (opts.dryRun) return update(opts);
|
|
577
|
+
verifyPayload(PAYLOAD(), payloadManifest());
|
|
578
|
+
return withUpdateLock(opts.home, () => update(opts));
|
|
579
|
+
},
|
|
580
|
+
restore: (opts = {}) => withUpdateLock(opts.home, () => restore(opts)),
|
|
581
|
+
uninstall: (opts = {}) => withUpdateLock(opts.home, () => uninstall(opts)),
|
|
582
|
+
status, setConfig, paths, CHANNELS, MARKETPLACE, payloadManifest, marketplaceAddArg,
|
|
583
|
+
acquireUpdateLock, UpdateRunning, staleReleases,
|
|
584
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "The PassionCode.ai agent skill set \u2014 Fabric Agent Adapter, Observatory Log and the organisation's working rules \u2014 for Claude Code and every other agent on the machine, updated as one set.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"name": "observatory-log",
|
|
24
24
|
"displayName": "Observatory Log",
|
|
25
25
|
"source": "./plugins/observatory-log",
|
|
26
|
-
"version": "0.
|
|
26
|
+
"version": "0.17.3",
|
|
27
27
|
"description": "Records what changed in a watched project at the end of every turn, and asks for the one thing a diff cannot contain: why.",
|
|
28
28
|
"author": {
|
|
29
29
|
"name": "PassionCode.ai",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.1.
|
|
38
|
+
"version": "0.1.29",
|
|
39
39
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
package/payload/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"name": "observatory-log",
|
|
35
35
|
"displayName": "Observatory Log",
|
|
36
36
|
"repo": "passioncode-ai/project-observatory-dashboard",
|
|
37
|
-
"ref": "v0.
|
|
38
|
-
"commit": "
|
|
39
|
-
"version": "0.
|
|
37
|
+
"ref": "v0.17.3",
|
|
38
|
+
"commit": "58bf0d9aabc452da95133fc561ee08d5279d6cf5",
|
|
39
|
+
"version": "0.17.3",
|
|
40
40
|
"via": "clone of https://***@github.com/passioncode-ai/project-observatory-dashboard.git",
|
|
41
41
|
"skills": [
|
|
42
42
|
"explaining-changes",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"legacyMarketplaces": [
|
|
50
50
|
"observatory-log"
|
|
51
51
|
],
|
|
52
|
-
"contentHash": "sha256:
|
|
52
|
+
"contentHash": "sha256:615c85cd2486ac990c04a1df834639ace0e22f2ab3d2830ccafa72453b3e0b3a",
|
|
53
53
|
"description": "Records what changed in a watched project at the end of every turn, and asks for the one thing a diff cannot contain: why.",
|
|
54
54
|
"author": {
|
|
55
55
|
"name": "PassionCode.ai",
|
|
@@ -61,16 +61,16 @@
|
|
|
61
61
|
"name": "passioncode",
|
|
62
62
|
"displayName": "PassionCode.ai",
|
|
63
63
|
"repo": "passioncode-ai/passioncode",
|
|
64
|
-
"ref": "v0.1.
|
|
64
|
+
"ref": "v0.1.29",
|
|
65
65
|
"commit": null,
|
|
66
|
-
"version": "0.1.
|
|
66
|
+
"version": "0.1.29",
|
|
67
67
|
"via": "this repository",
|
|
68
68
|
"skills": [
|
|
69
69
|
"working-in-passioncode"
|
|
70
70
|
],
|
|
71
71
|
"legacyPluginIds": [],
|
|
72
72
|
"legacyMarketplaces": [],
|
|
73
|
-
"contentHash": "sha256:
|
|
73
|
+
"contentHash": "sha256:61bfba01e7a1d988d4a68b31b11cabf695996b1d4285c1518189e80bb2795adc",
|
|
74
74
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
75
75
|
"author": {
|
|
76
76
|
"name": "PassionCode.ai",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "observatory-log",
|
|
4
4
|
"displayName": "Observatory Log",
|
|
5
5
|
"description": "Records what changed in a watched project at the end of every turn, and asks for the one thing a diff cannot contain: why.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.17.3",
|
|
7
7
|
"license": "AGPL-3.0-only OR LicenseRef-PassionCode-Commercial",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "PassionCode.ai",
|
|
@@ -18,10 +18,83 @@ if [ -z "$root" ] && [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
|
|
18
18
|
c="$n"
|
|
19
19
|
done
|
|
20
20
|
fi
|
|
21
|
+
if [ -z "$root" ] && command -v project-observatory >/dev/null 2>&1; then
|
|
22
|
+
# Installed by the PassionCode launcher with no `full agent install`: ask the engine.
|
|
23
|
+
root="$(project-observatory full-path 2>/dev/null || true)"
|
|
24
|
+
fi
|
|
21
25
|
[ -n "$root" ] || exit 0
|
|
22
|
-
[ -
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
[ -d "$root" ] || exit 0
|
|
27
|
+
|
|
28
|
+
# The engine's own virtual environment (the ancestor holding pyvenv.cfg), so the jobs
|
|
29
|
+
# below run the installed package and never whichever python3 is first on PATH.
|
|
30
|
+
venv=""; c="$root"
|
|
31
|
+
for _ in 1 2 3 4 5 6 7 8; do
|
|
32
|
+
[ -f "$c/pyvenv.cfg" ] && { venv="$c"; break; }
|
|
33
|
+
n="$(dirname "$c")"; [ "$n" = "$c" ] && break; c="$n"
|
|
34
|
+
done
|
|
35
|
+
py="${OBSERVATORY_PYTHON:-}" # set by `full agent install`
|
|
36
|
+
[ -n "$py" ] && [ -x "$py" ] || py=""
|
|
37
|
+
[ -z "$py" ] && [ -n "$venv" ] && [ -x "$venv/bin/python" ] && py="$venv/bin/python"
|
|
38
|
+
[ -z "$py" ] && [ -x "$root/.venv/bin/python" ] && py="$root/.venv/bin/python"
|
|
39
|
+
[ -z "$py" ] && py="$(command -v python3 2>/dev/null)"
|
|
25
40
|
[ -n "$py" ] || exit 0
|
|
26
|
-
|
|
41
|
+
|
|
42
|
+
if [ -f "$root/tools/session_start.py" ]; then
|
|
43
|
+
printf '%s' "$payload" | "$py" "$root/tools/session_start.py" 2>/dev/null || true
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Updates that arrive by themselves (docs/runs/2026-10-05-auto-update, D8). Detached and
|
|
47
|
+
# rate-limited, so a session never waits for it and never fails because of it.
|
|
48
|
+
stamps="${XDG_STATE_HOME:-$HOME/.local/state}/project-observatory"
|
|
49
|
+
mkdir -p "$stamps" 2>/dev/null && chmod 700 "$stamps" 2>/dev/null
|
|
50
|
+
# claim NAME SECONDS: true for exactly one caller per window. `mkdir` is atomic, so twenty
|
|
51
|
+
# sessions starting at once start one job, not twenty (review F6).
|
|
52
|
+
claim() {
|
|
53
|
+
local lock="$stamps/$1.claim" then now
|
|
54
|
+
if ! mkdir "$lock" 2>/dev/null; then
|
|
55
|
+
then="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null || echo 0)"
|
|
56
|
+
now="$(date +%s)"
|
|
57
|
+
[ $((now - then)) -ge "$2" ] || return 1
|
|
58
|
+
rmdir "$lock" 2>/dev/null; mkdir "$lock" 2>/dev/null || return 1
|
|
59
|
+
fi
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
# detach LOG CMD...: a new session of its own, so ending this session or its process group
|
|
63
|
+
# cannot stop an install half-way (review F8).
|
|
64
|
+
detach() {
|
|
65
|
+
local log="$1"; shift
|
|
66
|
+
"$py" -c 'import subprocess, sys
|
|
67
|
+
log = open(sys.argv[1], "a")
|
|
68
|
+
subprocess.Popen(sys.argv[2:], stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True)' \
|
|
69
|
+
"$log" "$@" 2>/dev/null || true
|
|
70
|
+
}
|
|
71
|
+
if [ -f "$root/tools/maintain.py" ]; then
|
|
72
|
+
# 0.17.0 and later: schedule the hourly job where it is missing, or run a pass where
|
|
73
|
+
# this machine has no scheduler. At most every six hours.
|
|
74
|
+
if claim maintain-hook 21600; then
|
|
75
|
+
OBSERVATORY_SYSTEM_SETUP=1 detach "$stamps/maintain-hook.log" "$py" "$root/tools/maintain.py" hook
|
|
76
|
+
fi
|
|
77
|
+
else
|
|
78
|
+
# The bridge for engines from before 0.17.0, which carry no updater of their own:
|
|
79
|
+
# once a day, check; when a newer stable release exists and the person has not turned
|
|
80
|
+
# automatic updates off (`updates.auto` false), install it with `full update --apply`.
|
|
81
|
+
po=""
|
|
82
|
+
[ -n "$venv" ] && [ -x "$venv/bin/project-observatory" ] && po="$venv/bin/project-observatory"
|
|
83
|
+
[ -n "$po" ] || po="$(command -v project-observatory 2>/dev/null)"
|
|
84
|
+
if [ -n "$po" ] && claim update-bridge 86400; then
|
|
85
|
+
detach "$stamps/update-bridge.log" /bin/bash -c '
|
|
86
|
+
po="$1"; py="$2"
|
|
87
|
+
home="${OBSERVATORY_FULL_HOME:-${OBSERVATORY_HOME:-$HOME/.local/share/project-observatory-full}}"
|
|
88
|
+
off="$("$py" -c "import json,sys
|
|
89
|
+
try: d=json.load(open(sys.argv[1]))
|
|
90
|
+
except Exception: d={}
|
|
91
|
+
print(1 if (d.get(\"updates\") or {}).get(\"auto\") is False else 0)" "$home/config/settings.json" 2>/dev/null)"
|
|
92
|
+
[ "$off" = "1" ] && { echo "$(date -u +%FT%TZ) automatic updates are off"; exit 0; }
|
|
93
|
+
"$po" full update --check >/dev/null 2>&1; rc=$?
|
|
94
|
+
echo "$(date -u +%FT%TZ) check exit $rc"
|
|
95
|
+
[ "$rc" = "10" ] || exit 0
|
|
96
|
+
"$po" full update --apply; echo "$(date -u +%FT%TZ) apply exit $?"
|
|
97
|
+
' _ "$po" "$py"
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
27
100
|
exit 0
|
|
@@ -15,7 +15,7 @@ description: >-
|
|
|
15
15
|
decision with different evidence.
|
|
16
16
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
17
17
|
metadata:
|
|
18
|
-
version: "0.
|
|
18
|
+
version: "0.17.3"
|
|
19
19
|
compatibility: >-
|
|
20
20
|
Requires an initialized full Project Observatory installation to persist
|
|
21
21
|
records. Uses its MCP server when available in the current host; the bundled
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
provider permissions or choosing a project's authentication architecture.
|
|
11
11
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
12
12
|
metadata:
|
|
13
|
-
version: "0.
|
|
13
|
+
version: "0.17.3"
|
|
14
14
|
compatibility: >-
|
|
15
15
|
Requires an initialized full Project Observatory installation, Python 3.11+
|
|
16
16
|
and local shell access on macOS or Linux. Provider operations additionally
|
|
@@ -30,7 +30,7 @@ names. Never ask the user to paste a credential into the conversation.
|
|
|
30
30
|
2. Select the user's initialized `OBSERVATORY_HOME`. Run
|
|
31
31
|
`project-observatory full doctor`. A missing workspace
|
|
32
32
|
needs the documented onboarding before secret operations.
|
|
33
|
-
3. Run `"$OBSERVATORY_PYTHON" "$OBSERVATORY_ROOT/tools/skill_check.py" handling-secrets 0.
|
|
33
|
+
3. Run `"$OBSERVATORY_PYTHON" "$OBSERVATORY_ROOT/tools/skill_check.py" handling-secrets 0.17.3`
|
|
34
34
|
(`full agent install` records that interpreter; the stock macOS `python3` is too old).
|
|
35
35
|
If stale, read the installed skill once and follow its compatible commands.
|
|
36
36
|
Do not turn an unavailable version check into a retry loop.
|
|
@@ -154,6 +154,16 @@ bucket's resource only. Before anything is delivered, the S3 pair must prove a
|
|
|
154
154
|
put, a get and a delete, and must be refused a bucket list. It lands in three
|
|
155
155
|
slots on stdin: `<PREFIX>_ACCESS_KEY_ID`, `<PREFIX>_SECRET_ACCESS_KEY` and
|
|
156
156
|
`<PREFIX>_ENDPOINT`. A second issue rolls the same token and keeps the key id.
|
|
157
|
+
The lifecycle is `--expire-days` over the whole bucket (30 when nothing else is
|
|
158
|
+
given) plus one rule per repeatable `--lifecycle-rule PREFIX:DAYS`, for example
|
|
159
|
+
`--lifecycle-rule staging/:2`: objects under the prefix are deleted after DAYS,
|
|
160
|
+
and its unfinished multipart uploads are aborted after a day. Each run replaces
|
|
161
|
+
the bucket's lifecycle with exactly that set and reads every rule back. A rule
|
|
162
|
+
that could never fire is refused, such as a prefix kept longer than the
|
|
163
|
+
whole-bucket rule. To change only an existing bucket's rules, run
|
|
164
|
+
`cloudflare.py lifecycle --account <slug> --bucket <name> [--jurisdiction eu]
|
|
165
|
+
[--expire-days N] [--lifecycle-rule PREFIX:DAYS ...]`. It mints only the setup
|
|
166
|
+
token, never creates a bucket and never mints, rolls or delivers a key.
|
|
157
167
|
|
|
158
168
|
An application that sends transactional email through Cloudflare Email Service
|
|
159
169
|
gets `cloudflare.py issue --preset email-send --account <slug> --vault
|
|
@@ -15,7 +15,7 @@ description: >-
|
|
|
15
15
|
the repository.
|
|
16
16
|
license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
17
17
|
metadata:
|
|
18
|
-
version: "0.
|
|
18
|
+
version: "0.17.3"
|
|
19
19
|
compatibility: >-
|
|
20
20
|
Requires a full Project Observatory installation with organizations.json
|
|
21
21
|
configured; the observatory MCP server or its local CLI. Creating the resource
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "passioncode",
|
|
4
4
|
"displayName": "PassionCode.ai",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.29",
|
|
6
6
|
"description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check at session start that keeps the PassionCode.ai skill set current.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
@@ -6,11 +6,15 @@ const { execFile } = require('child_process');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const { PACKAGE, parseView, recordProbe } = require('./update-check');
|
|
9
|
+
const { PACKAGE, parseView, recordProbe, releaseProbe } = require('./update-check');
|
|
10
10
|
|
|
11
11
|
const DIR = process.env.PASSIONCODE_HOME || path.join(os.homedir(), '.passioncode');
|
|
12
12
|
const STATE = path.join(DIR, 'state.json');
|
|
13
13
|
execFile('npm', ['view', PACKAGE, 'version', 'maintainers', '_npmUser', '--json'], { timeout: 30000 }, (error, stdout) => {
|
|
14
|
+
try { record(error, stdout); } finally { releaseProbe(DIR); } // the hook claimed probe.pending before it started us
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function record(error, stdout) {
|
|
14
18
|
let state = {};
|
|
15
19
|
try { state = JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch (_) { /* first run */ }
|
|
16
20
|
const result = parseView(stdout, error);
|
|
@@ -20,4 +24,4 @@ execFile('npm', ['view', PACKAGE, 'version', 'maintainers', '_npmUser', '--json'
|
|
|
20
24
|
fs.writeFileSync(tmp, JSON.stringify(next, null, 2));
|
|
21
25
|
fs.renameSync(tmp, STATE);
|
|
22
26
|
process.stdout.write(`${next.checkedAt} ${result.checkError ? `check failed: ${result.checkError}` : result.published ? `latest ${result.latest} by ${result.maintainers.join(', ') || '(unreadable maintainers)'}` : `${PACKAGE} is not published`}\n`);
|
|
23
|
-
}
|
|
27
|
+
}
|
|
@@ -9,7 +9,7 @@ const { spawn } = require('child_process');
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const path = require('path');
|
|
12
|
-
const { decide, loadTrust } = require('./update-check');
|
|
12
|
+
const { decide, loadTrust, claimProbe, releaseProbe } = require('./update-check');
|
|
13
13
|
const { rulesLine } = require('./repo-rules');
|
|
14
14
|
|
|
15
15
|
const DIR = process.env.PASSIONCODE_HOME || path.join(os.homedir(), '.passioncode');
|
|
@@ -38,8 +38,9 @@ async function main() {
|
|
|
38
38
|
const state = read(STATE, {});
|
|
39
39
|
const now = Date.now();
|
|
40
40
|
const age = now - Date.parse(state.checkedAt);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// The marker is claimed before the spawn: sessions restored together start one probe.
|
|
42
|
+
if ((!Number.isFinite(age) || age < 0 || age > DAY) && claimProbe(DIR, now)) {
|
|
43
|
+
if (!await detached(process.execPath, [path.join(__dirname, 'probe.js')], 'probe.log')) releaseProbe(DIR);
|
|
43
44
|
}
|
|
44
45
|
const { lines, spawn: args, stateChanges } = decide(state, loadTrust(), now);
|
|
45
46
|
if (Object.keys(stateChanges).length) {
|
|
@@ -102,6 +102,38 @@ function newer(a, b) {
|
|
|
102
102
|
return false;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* One probe at a time. The probe writes `checkedAt` only when npm answers (up to 30 s), so every
|
|
107
|
+
* session that starts meanwhile saw a stale check and started its own `npm view`. The hook now
|
|
108
|
+
* claims `probe.pending` with an exclusive create BEFORE it spawns; the probe removes it after it
|
|
109
|
+
* has recorded. A marker older than PROBE_STALE_MS (or dated in the future) belongs to a probe
|
|
110
|
+
* that died: it is moved aside atomically — so two hooks cannot both take it over — and claimed.
|
|
111
|
+
*/
|
|
112
|
+
const PROBE_MARKER = 'probe.pending';
|
|
113
|
+
const PROBE_STALE_MS = 5 * 60 * 1000;
|
|
114
|
+
|
|
115
|
+
function claimProbe(dir, now = Date.now()) {
|
|
116
|
+
const file = path.join(dir, PROBE_MARKER);
|
|
117
|
+
const create = () => { fs.writeFileSync(file, new Date(now).toISOString(), { flag: 'wx', mode: 0o600 }); return true; };
|
|
118
|
+
try {
|
|
119
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
120
|
+
return create();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.code !== 'EEXIST') return false;
|
|
123
|
+
}
|
|
124
|
+
let age;
|
|
125
|
+
try { age = now - fs.statSync(file).mtimeMs; } catch (_) { return false; } // just released: the next session checks
|
|
126
|
+
if (age >= 0 && age < PROBE_STALE_MS) return false;
|
|
127
|
+
const aside = `${file}.stale.${process.pid}`;
|
|
128
|
+
try { fs.renameSync(file, aside); } catch (_) { return false; } // another hook took it over
|
|
129
|
+
fs.rmSync(aside, { force: true });
|
|
130
|
+
try { return create(); } catch (_) { return false; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function releaseProbe(dir) {
|
|
134
|
+
fs.rmSync(path.join(dir, PROBE_MARKER), { force: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
105
137
|
function oneLine(text) { return String(text).replace(/\s+/g, ' ').trim().slice(0, 200); }
|
|
106
138
|
|
|
107
139
|
/**
|
|
@@ -144,4 +176,4 @@ function decide(state, trust, now = Date.now()) {
|
|
|
144
176
|
return { lines, spawn, stateChanges };
|
|
145
177
|
}
|
|
146
178
|
|
|
147
|
-
module.exports = { PACKAGE, OIDC_PUBLISHER, TRUST_FILE, accountName, parseView, recordProbe, loadTrust, newer, decide };
|
|
179
|
+
module.exports = { PACKAGE, OIDC_PUBLISHER, TRUST_FILE, PROBE_MARKER, PROBE_STALE_MS, accountName, parseView, recordProbe, loadTrust, newer, decide, claimProbe, releaseProbe };
|
|
@@ -152,16 +152,18 @@ Fabric's workspace sync, and check the published source commit before calling it
|
|
|
152
152
|
## 6. Licensing and the repository standard
|
|
153
153
|
|
|
154
154
|
- **Every repository** is open source under the GNU AGPL-3.0, or available under a commercial
|
|
155
|
-
license from PassionCode.ai
|
|
156
|
-
`AGPL-3.0-only OR LicenseRef-PassionCode-Commercial` (Fabric
|
|
157
|
-
`licensing.md` owns it). Copyright: Siarhei Sheleh. No price or
|
|
155
|
+
license from PassionCode.ai — the request form https://passioncode.ai/business/, or
|
|
156
|
+
commercial@passioncode.ai: `AGPL-3.0-only OR LicenseRef-PassionCode-Commercial` (Fabric
|
|
157
|
+
ADR-0092; the knowledge base `licensing.md` owns it). Copyright: Siarhei Sheleh. No price or
|
|
158
|
+
term is stated anywhere; every commercial path points to the form. Security reports stay at
|
|
159
|
+
contact@passioncode.ai.
|
|
158
160
|
- `LICENSE`, `COMMERCIAL-LICENSE.md` and `CLA.md` are copied **byte for byte** from the knowledge
|
|
159
161
|
base's `templates/`; every manifest and SKILL.md `license:` carries the expression.
|
|
160
162
|
- A version released earlier keeps its licence — MIT or PolyForm Noncommercial or Internal Use —
|
|
161
163
|
and the README may say which. Never write that a released version changed.
|
|
162
164
|
- The README's `## License` section uses the knowledge base wording and names the earlier
|
|
163
165
|
licences: "Open source under the GNU AGPL-3.0. A commercial license is available for use that
|
|
164
|
-
does not meet the AGPL's terms —
|
|
166
|
+
does not meet the AGPL's terms — [passioncode.ai/business](https://passioncode.ai/business/)."
|
|
165
167
|
- Contributions are accepted under the repository's `CLA.md`; opening a pull request is the
|
|
166
168
|
agreement, so a PR template never carries a CLA checkbox.
|
|
167
169
|
- **The repository standard** (the knowledge base `repository-standard.md`): README first heading
|
|
@@ -14,7 +14,7 @@ change to a license, a manifest's `license` field, `CLA.md` or a PR template.
|
|
|
14
14
|
## The license, file by file
|
|
15
15
|
|
|
16
16
|
Every repository is open source under the GNU AGPL-3.0, or available under a commercial
|
|
17
|
-
license from PassionCode.ai (
|
|
17
|
+
license from PassionCode.ai (https://passioncode.ai/business/, commercial@passioncode.ai): SPDX
|
|
18
18
|
`AGPL-3.0-only OR LicenseRef-PassionCode-Commercial` (Fabric ADR-0092). The owning page is the
|
|
19
19
|
knowledge base's `licensing.md`; the file list is its `repository-standard.md`, and org-index
|
|
20
20
|
`scripts/check_format.py` (rules F7–F11) checks it. The three licence files are **copied byte
|
|
@@ -47,14 +47,15 @@ for byte** from the knowledge base's `templates/` — never retyped, reflowed or
|
|
|
47
47
|
## License
|
|
48
48
|
|
|
49
49
|
Open source under the [GNU AGPL-3.0](LICENSE). A [commercial license](COMMERCIAL-LICENSE.md) is
|
|
50
|
-
available for use that does not meet the AGPL's terms —
|
|
50
|
+
available for use that does not meet the AGPL's terms — [passioncode.ai/business](https://passioncode.ai/business/).
|
|
51
51
|
Versions before <first AGPL version> were released under <MIT | PolyForm Noncommercial or Internal Use>.
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
- "Source-available" and the PolyForm expression describe past releases only; never state
|
|
55
55
|
them as the current licence.
|
|
56
56
|
- No price or term for the commercial licence is stated anywhere; point to
|
|
57
|
-
|
|
57
|
+
https://passioncode.ai/business/ (commercial@passioncode.ai is the commercial contact). Security
|
|
58
|
+
reports stay at contact@passioncode.ai.
|
|
58
59
|
- Positioning and names come from the knowledge base (`vision.md`, `principles.md` §2); do not
|
|
59
60
|
restate a tagline from memory.
|
|
60
61
|
|