@ainova-systems/intelligence 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/sync.sh +9 -0
- package/cli/lib/sync-lock.sh +103 -21
- package/engine/ENGINE_SHA +1 -1
- package/engine/VERSION +1 -1
- package/engine/adapters/claude-plugin/.claude-plugin/plugin.json +6 -0
- package/engine/adapters/claude-plugin/README.md +14 -0
- package/engine/adapters/claude-plugin/hooks/hooks.json +3 -0
- package/engine/adapters/claude-plugin/hooks/register.js +324 -0
- package/engine/adapters/claude.sh +93 -2
- package/engine/adapters/copilot.sh +6 -12
- package/engine/lib/common.sh +18 -0
- package/engine/lib/contract.sh +15 -6
- package/package.json +1 -1
- package/packages/sync/references/adapters.md +12 -1
package/cli/commands/sync.sh
CHANGED
|
@@ -106,6 +106,10 @@ run_check() {
|
|
|
106
106
|
die "no intelligence project found here - run 'intelligence init'"
|
|
107
107
|
;;
|
|
108
108
|
esac
|
|
109
|
+
# A check marks the lock it holds, so a sync started meanwhile waits for
|
|
110
|
+
# it instead of refusing (sync-lock.sh).
|
|
111
|
+
# shellcheck disable=SC2034 # read by sync-lock.sh when the lock is taken
|
|
112
|
+
SYNC_LOCK_KIND=check
|
|
109
113
|
hold_project
|
|
110
114
|
project_preflight "$IP_ROOT" || return $?
|
|
111
115
|
if project_needs_upgrade "$IP_ROOT"; then
|
|
@@ -144,6 +148,11 @@ fi
|
|
|
144
148
|
compact_output="$(mktemp -t intelligence-sync-XXXXXX)"
|
|
145
149
|
trap 'rm -f "$compact_output"' EXIT
|
|
146
150
|
rc=0
|
|
151
|
+
# The one line that must not wait for the buffer: a wait for a check holding
|
|
152
|
+
# the lock can last minutes (sync-lock.sh). Descriptor 9 keeps the real stderr.
|
|
153
|
+
exec 9>&2
|
|
154
|
+
# shellcheck disable=SC2034 # read by sync-lock.sh while the lock is taken
|
|
155
|
+
IS_LOCK_NOTICE_FD=9
|
|
147
156
|
(run_sync) > "$compact_output" 2>&1 || rc=$?
|
|
148
157
|
if [ "$rc" -ne 0 ]; then
|
|
149
158
|
cat "$compact_output" >&2
|
package/cli/lib/sync-lock.sh
CHANGED
|
@@ -12,9 +12,25 @@
|
|
|
12
12
|
# join instead of refusing, and a holder that died can be told from one whose
|
|
13
13
|
# writers still run. Taking over a dead holder's lock happens under a second
|
|
14
14
|
# lock, so two runs that judged the same lock stale cannot both take it.
|
|
15
|
+
#
|
|
16
|
+
# `sync --check` holds the lock too, because it may restore the package store
|
|
17
|
+
# and compares a copy of the live outputs. It writes nothing a person keeps, so
|
|
18
|
+
# a holder that is a check marks its lock with a `kind` file, and a writing
|
|
19
|
+
# command that finds such a holder waits for it instead of refusing (decision
|
|
20
|
+
# 0022): a status check that runs every few minutes must never make the
|
|
21
|
+
# person's own sync fail. A check never waits in line — it refuses any holder
|
|
22
|
+
# at once — so it is never queued behind a writer; one that arrives as a lock is
|
|
23
|
+
# released can still take it first, and the waiting writer's bound then starts
|
|
24
|
+
# again for that check. A writing holder is refused at once, as before.
|
|
15
25
|
|
|
16
26
|
SYNC_LOCK_HELD=""
|
|
17
27
|
SYNC_LOCK_TOKEN=""
|
|
28
|
+
# What this process takes the lock for: "check" for `sync --check`, empty for
|
|
29
|
+
# every writer. Set before the lock is taken.
|
|
30
|
+
SYNC_LOCK_KIND=""
|
|
31
|
+
# How long a writer waits for each check that holds the lock, in seconds. Kept
|
|
32
|
+
# in the environment only so the lock suite can exercise the bound quickly.
|
|
33
|
+
SYNC_LOCK_CHECK_WAIT="${INTELLIGENCE_SYNC_LOCK_CHECK_WAIT:-300}"
|
|
18
34
|
|
|
19
35
|
sync_lock_now() {
|
|
20
36
|
if [ -n "${EPOCHSECONDS:-}" ]; then
|
|
@@ -47,6 +63,9 @@ sync_lock_pgid() {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
sync_lock_write_owner() {
|
|
66
|
+
# The kind goes first, before anything that costs time: whoever can read
|
|
67
|
+
# the owner can read the kind too.
|
|
68
|
+
[ -z "$SYNC_LOCK_KIND" ] || printf '%s\n' "$SYNC_LOCK_KIND" > "$1/kind"
|
|
50
69
|
sync_lock_now
|
|
51
70
|
sync_lock_pgid
|
|
52
71
|
SYNC_LOCK_TOKEN="$$.$IS_NOW.$RANDOM$RANDOM"
|
|
@@ -88,31 +107,89 @@ sync_lock_is_stale() {
|
|
|
88
107
|
[ -d "$1" ] && [ -n "$(find "$1" -maxdepth 0 -mmin +1 2>/dev/null)" ]
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
#
|
|
110
|
+
# sync_lock_held_by_check <lock> — true when the holder marked its lock as a
|
|
111
|
+
# `sync --check`. A lock without the mark is a writer's, or an older CLI's.
|
|
112
|
+
sync_lock_held_by_check() {
|
|
113
|
+
local kind=""
|
|
114
|
+
# The group silences the redirection too: a kind released between the
|
|
115
|
+
# test and the read must not print a stray error a caller would show.
|
|
116
|
+
[ -f "$1/kind" ] && { IFS= read -r kind < "$1/kind"; } 2>/dev/null
|
|
117
|
+
[ "$kind" = check ]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# sync_lock_acquire <root> — take <root>/.intelligence/sync.lock, or refuse.
|
|
121
|
+
# A writer waits for each check that holds it for up to SYNC_LOCK_CHECK_WAIT
|
|
122
|
+
# seconds; a check refuses at once.
|
|
92
123
|
sync_lock_acquire() {
|
|
93
|
-
local root="$1" lock="$1/.intelligence/sync.lock" took=0
|
|
124
|
+
local root="$1" lock="$1/.intelligence/sync.lock" took age="" started="$SECONDS" told=0 ownerless=""
|
|
125
|
+
local holder="" seen
|
|
94
126
|
# A process start costs tens of milliseconds on Git Bash: skip the usual case.
|
|
95
127
|
[ -d "$root/.intelligence" ] || mkdir -p "$root/.intelligence" || die "cannot create $root/.intelligence"
|
|
96
|
-
|
|
97
|
-
took=
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
while :; do
|
|
129
|
+
took=0
|
|
130
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
131
|
+
took=1
|
|
132
|
+
elif sync_lock_is_stale "$lock" && mkdir "$lock.takeover" 2>/dev/null; then
|
|
133
|
+
# The takeover lock is taken only for a lock that already looks
|
|
134
|
+
# stale: a command waiting for a live check must never hold it,
|
|
135
|
+
# since one interrupted mid-wait would leave it behind for good.
|
|
136
|
+
# Judge again under it: another run may have taken the lock over
|
|
137
|
+
# since this one looked.
|
|
138
|
+
if sync_lock_is_stale "$lock"; then
|
|
139
|
+
rm -rf "$lock"
|
|
140
|
+
mkdir "$lock" 2>/dev/null && took=1
|
|
141
|
+
fi
|
|
142
|
+
rmdir "$lock.takeover"
|
|
143
|
+
elif [ -d "$lock.takeover" ] && [ -n "$(find "$lock.takeover" -maxdepth 0 -mmin +1 2>/dev/null)" ]; then
|
|
144
|
+
# A takeover runs for milliseconds. Removing an abandoned one is itself
|
|
145
|
+
# a check-then-delete race, so it is left to a person.
|
|
146
|
+
die "a takeover of $lock was abandoned; if no intelligence command is running in this project, remove $lock.takeover"
|
|
104
147
|
fi
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
148
|
+
if [ "$took" = 1 ]; then
|
|
149
|
+
sync_lock_write_owner "$lock" || { rm -rf "$lock"; die "cannot write $lock/owner"; }
|
|
150
|
+
SYNC_LOCK_HELD="$lock"
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
if [ "$SYNC_LOCK_KIND" != check ] && sync_lock_held_by_check "$lock"; then
|
|
154
|
+
# A later ownerless moment belongs to another holder's start or
|
|
155
|
+
# release, and gets its own few seconds.
|
|
156
|
+
ownerless=""
|
|
157
|
+
# The bound is per check: one that took the lock while this
|
|
158
|
+
# command waited starts its own count.
|
|
159
|
+
if sync_lock_read_owner "$lock"; then
|
|
160
|
+
seen="$IS_LOCK_PID $IS_LOCK_SINCE $IS_LOCK_TOKEN"
|
|
161
|
+
if [ "$seen" != "$holder" ]; then
|
|
162
|
+
holder="$seen"
|
|
163
|
+
started="$SECONDS"
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
[ $((SECONDS - started)) -lt "$SYNC_LOCK_CHECK_WAIT" ] || break
|
|
167
|
+
if [ "$told" = 0 ]; then
|
|
168
|
+
# A caller that buffers this command's output (sync --compact)
|
|
169
|
+
# names a descriptor for the terminal, so a wait of minutes
|
|
170
|
+
# is never silent.
|
|
171
|
+
# Not one `>&"${fd:-2}"`: the group's 2>/dev/null, which keeps
|
|
172
|
+
# a closed descriptor quiet, would swallow the default too.
|
|
173
|
+
if [ -n "${IS_LOCK_NOTICE_FD:-}" ]; then
|
|
174
|
+
{ echo "waiting for intelligence sync --check to release $lock" >&"$IS_LOCK_NOTICE_FD"; } 2>/dev/null \
|
|
175
|
+
|| echo "waiting for intelligence sync --check to release $lock" >&2
|
|
176
|
+
else
|
|
177
|
+
echo "waiting for intelligence sync --check to release $lock" >&2
|
|
178
|
+
fi
|
|
179
|
+
told=1
|
|
180
|
+
fi
|
|
181
|
+
elif ! sync_lock_read_owner "$lock"; then
|
|
182
|
+
# No owner: a holder between its mkdir and its owner file, or one
|
|
183
|
+
# being released. Both last moments, so look again for a few
|
|
184
|
+
# seconds from the first sight before calling it a run that is
|
|
185
|
+
# still starting.
|
|
186
|
+
[ -n "$ownerless" ] || ownerless="$SECONDS"
|
|
187
|
+
[ $((SECONDS - ownerless)) -lt 3 ] || break
|
|
188
|
+
else
|
|
189
|
+
break
|
|
190
|
+
fi
|
|
191
|
+
sleep 1
|
|
192
|
+
done
|
|
116
193
|
if sync_lock_read_owner "$lock"; then
|
|
117
194
|
sync_lock_now
|
|
118
195
|
case "$IS_LOCK_SINCE" in ''|*[!0-9]*) ;; *) age=", for $((IS_NOW - IS_LOCK_SINCE))s" ;; esac
|
|
@@ -147,6 +224,11 @@ project_lock_hold() {
|
|
|
147
224
|
sync_lock_release() {
|
|
148
225
|
[ -n "$SYNC_LOCK_HELD" ] || return 0
|
|
149
226
|
if sync_lock_read_owner "$SYNC_LOCK_HELD" && [ "$IS_LOCK_TOKEN" = "$SYNC_LOCK_TOKEN" ]; then
|
|
227
|
+
# A check's owner goes first: `rm -rf` alone could expose a moment with
|
|
228
|
+
# the owner still there and the kind gone, and a command waiting for
|
|
229
|
+
# the check would be refused in it. Without the owner, a waiter sees a
|
|
230
|
+
# check or a lock still starting, and waits either way.
|
|
231
|
+
[ ! -f "$SYNC_LOCK_HELD/kind" ] || rm -f "$SYNC_LOCK_HELD/owner"
|
|
150
232
|
rm -rf "$SYNC_LOCK_HELD"
|
|
151
233
|
fi
|
|
152
234
|
SYNC_LOCK_HELD=""
|
package/engine/ENGINE_SHA
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ee05b0f09bc74b1914e04c067cce1e87a75bd18a
|
package/engine/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.22.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Intelligence sync status for Claude Code
|
|
2
|
+
|
|
3
|
+
`intelligence sync` renders this plugin into a project's
|
|
4
|
+
`.claude/skills/intelligence/` from the Intelligence engine's
|
|
5
|
+
`adapters/claude-plugin/` template when `targets.claude.plugin` is `true` in
|
|
6
|
+
`intelligence.yaml`. Change the template in the engine, never a rendered copy:
|
|
7
|
+
the next sync rewrites the copy, and setting the flag to `false` removes it.
|
|
8
|
+
|
|
9
|
+
It is a Claude Code plugin whose hooks module (`hooks/register.js`) checks
|
|
10
|
+
`intelligence sync --check` when a session starts and every five minutes,
|
|
11
|
+
shows the result in a line above the prompt while something needs attention,
|
|
12
|
+
and runs `intelligence sync` from a `Sync now` button there or the
|
|
13
|
+
`/intelligence sync` command. Its requirements and limits are defined in the
|
|
14
|
+
Intelligence CLI reference, `docs/cli.md`, under `intelligence sync`.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Intelligence sync status for Claude Code.
|
|
2
|
+
//
|
|
3
|
+
// `intelligence sync` renders this file from the engine's claude-plugin
|
|
4
|
+
// template when targets.claude.plugin is true (decision 0022). It is a Claude
|
|
5
|
+
// Code mod: a hooks module with no Node.js APIs, reaching the project only
|
|
6
|
+
// through the mods API `$`. It asks `intelligence sync --check` whether the
|
|
7
|
+
// generated output is current when a session starts and every five minutes,
|
|
8
|
+
// shows the answer in one line above the prompt while a check runs or
|
|
9
|
+
// something needs attention, and runs `intelligence sync` from a button there
|
|
10
|
+
// or the /intelligence command. It registers no tool and never submits a
|
|
11
|
+
// prompt: it informs the person, never Claude.
|
|
12
|
+
|
|
13
|
+
// A cold `sync --check` renders the project in a private copy, which takes
|
|
14
|
+
// minutes on Git Bash; a warm one answers from its record in seconds. The
|
|
15
|
+
// timeout stays just under the interval, so a slow check is waited for rather
|
|
16
|
+
// than reported as a failure. A tick never starts a check while one of ours is
|
|
17
|
+
// pending; one abandoned at the timeout can still finish beside the next.
|
|
18
|
+
const CHECK_EVERY_MS = 300000
|
|
19
|
+
const CHECK_TIMEOUT_MS = 290000
|
|
20
|
+
// `$.process.run` allows ten minutes at most, which a full sync may need: it
|
|
21
|
+
// first waits for any check holding the project lock (decision 0022).
|
|
22
|
+
const SYNC_TIMEOUT_MS = 600000
|
|
23
|
+
// Another command holding the project lock is asked again this soon, then
|
|
24
|
+
// after doubling pauses up to the regular interval, so a lock that stays held
|
|
25
|
+
// costs a few processes, not one every 15 seconds.
|
|
26
|
+
const RETRY_MS = 15000
|
|
27
|
+
// The spinner Claude Code draws while it works.
|
|
28
|
+
const FRAMES = ['·', '✢', '✳', '✶', '✻', '✽', '✻', '✶', '✳', '✢']
|
|
29
|
+
const FRAME_MS = 120
|
|
30
|
+
// The CLI's refusal when another command holds the project lock.
|
|
31
|
+
const LOCK_REFUSAL = /another intelligence sync is (running|starting)/
|
|
32
|
+
// The CLI's word that the project was stamped by a newer CLI than itself: a
|
|
33
|
+
// warning for a newer minor or patch, a refusal for a newer major.
|
|
34
|
+
const CLI_BEHIND = /newer than this CLI's engine|IS_STATUS=ahead-of-engine/
|
|
35
|
+
|
|
36
|
+
// What the last check found. `status` is one of unknown (no verdict yet), ok,
|
|
37
|
+
// stale, error, missing-cli or old-cli; `busy` is true while a sync started
|
|
38
|
+
// from here is running.
|
|
39
|
+
const state = { status: 'unknown', detail: '', checkedAt: null, busy: false }
|
|
40
|
+
// A check already in flight answers the next caller too, so a timer tick and
|
|
41
|
+
// a /intelligence check never run two CLI processes side by side.
|
|
42
|
+
let pendingCheck = null
|
|
43
|
+
let retry = null
|
|
44
|
+
let retryMs = RETRY_MS
|
|
45
|
+
let ticker = null
|
|
46
|
+
let frame = 0
|
|
47
|
+
// The terminal and the Desktop app draw the band above the prompt; elsewhere,
|
|
48
|
+
// such as the VS Code extension, the status line under the prompt carries the
|
|
49
|
+
// text. The footer under the prompt is Claude Code's own and stays untouched.
|
|
50
|
+
let band = false
|
|
51
|
+
|
|
52
|
+
// The CLI reports its result as one `IS_STATUS=<status> IS_DETAIL=<text>`
|
|
53
|
+
// line; everything else on stdout is progress.
|
|
54
|
+
function parseStatus(stdout) {
|
|
55
|
+
const match = /^IS_STATUS=(\S+)(?: IS_DETAIL=(.*))?$/m.exec(stdout || '')
|
|
56
|
+
return { status: match ? match[1] : '', detail: match && match[2] ? match[2].trim() : '' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function firstLine(text) {
|
|
60
|
+
const line = (text || '').split(/\r?\n/).find((l) => l.trim() !== '')
|
|
61
|
+
return line ? line.trim() : ''
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A failing CLI prints its progress before the reason, so the first line is
|
|
65
|
+
// often a banner: prefer the line that says what went wrong.
|
|
66
|
+
function reasonLine(text) {
|
|
67
|
+
const line = (text || '').split(/\r?\n/).find((l) => /^\s*ERROR:/.test(l))
|
|
68
|
+
return line ? line.trim().replace(/^ERROR:\s*/, '') : firstLine(text)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function errorMessage(err) {
|
|
72
|
+
return String(err && err.message ? err.message : err)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clock(date) {
|
|
76
|
+
const hh = String(date.getHours()).padStart(2, '0')
|
|
77
|
+
const mm = String(date.getMinutes()).padStart(2, '0')
|
|
78
|
+
return hh + ':' + mm
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setState(status, detail) {
|
|
82
|
+
// A verdict ends a run of retries; the next held lock starts at 15 s again.
|
|
83
|
+
retryMs = RETRY_MS
|
|
84
|
+
state.status = status
|
|
85
|
+
state.detail = detail || ''
|
|
86
|
+
state.checkedAt = new Date()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function animating() {
|
|
90
|
+
return state.busy || state.status === 'unknown'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The band's line: one short phrase, with Claude Code's spinner while a check
|
|
94
|
+
// or a sync has not answered yet.
|
|
95
|
+
function label() {
|
|
96
|
+
if (state.busy) return FRAMES[frame] + ' intelligence: syncing'
|
|
97
|
+
switch (state.status) {
|
|
98
|
+
case 'stale':
|
|
99
|
+
return 'intelligence: sync needed'
|
|
100
|
+
case 'missing-cli':
|
|
101
|
+
return 'intelligence: CLI not found'
|
|
102
|
+
case 'old-cli':
|
|
103
|
+
return 'intelligence: update the CLI'
|
|
104
|
+
case 'error':
|
|
105
|
+
return 'intelligence: check failed'
|
|
106
|
+
default:
|
|
107
|
+
return FRAMES[frame] + ' intelligence: checking'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The full sentence: the /intelligence reply, and the status line where no
|
|
112
|
+
// band draws. Claude Code prefixes a status line with the plugin's name.
|
|
113
|
+
function statusText() {
|
|
114
|
+
if (state.busy) return 'syncing…'
|
|
115
|
+
const at = state.checkedAt ? ' · ' + clock(state.checkedAt) : ''
|
|
116
|
+
switch (state.status) {
|
|
117
|
+
case 'ok':
|
|
118
|
+
return 'up to date' + at
|
|
119
|
+
case 'stale':
|
|
120
|
+
return 'sync needed — run /intelligence sync' + at
|
|
121
|
+
case 'missing-cli':
|
|
122
|
+
return 'CLI not found on PATH'
|
|
123
|
+
case 'old-cli':
|
|
124
|
+
return 'the intelligence CLI on PATH is older than this project — run: intelligence upgrade'
|
|
125
|
+
case 'error':
|
|
126
|
+
return 'check failed: ' + state.detail + at
|
|
127
|
+
default:
|
|
128
|
+
return 'checking…'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Every state change redraws the band above the prompt; the spinner turns only
|
|
133
|
+
// while something is pending.
|
|
134
|
+
function show($) {
|
|
135
|
+
if (animating() && !ticker) {
|
|
136
|
+
ticker = $.clock.every(FRAME_MS, () => {
|
|
137
|
+
frame = (frame + 1) % FRAMES.length
|
|
138
|
+
$.ui.invalidate('ui.render')
|
|
139
|
+
})
|
|
140
|
+
} else if (!animating() && ticker) {
|
|
141
|
+
ticker.cancel()
|
|
142
|
+
ticker = null
|
|
143
|
+
}
|
|
144
|
+
$.ui.status(band ? undefined : statusText())
|
|
145
|
+
$.ui.invalidate('ui.render')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Ask again soon after a command that held the project lock; the verdict
|
|
149
|
+
// shown meanwhile is the last one, never a guess.
|
|
150
|
+
function retrySoon($) {
|
|
151
|
+
if (retry) return
|
|
152
|
+
retry = $.clock.after(retryMs, () => {
|
|
153
|
+
retry = null
|
|
154
|
+
check($)
|
|
155
|
+
})
|
|
156
|
+
retryMs = Math.min(retryMs * 2, CHECK_EVERY_MS)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function check($) {
|
|
160
|
+
if (state.busy) return
|
|
161
|
+
if (pendingCheck) return pendingCheck
|
|
162
|
+
pendingCheck = runCheck($).finally(() => {
|
|
163
|
+
pendingCheck = null
|
|
164
|
+
})
|
|
165
|
+
return pendingCheck
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function runCheck($) {
|
|
169
|
+
let result
|
|
170
|
+
let cwd = ''
|
|
171
|
+
try {
|
|
172
|
+
// The project root, not the shell's cwd: a session started in a
|
|
173
|
+
// subdirectory still checks the project the plugin was rendered for.
|
|
174
|
+
cwd = await $.session.root()
|
|
175
|
+
result = await $.process.run(['intelligence', 'sync', '--check'], { cwd, timeoutMs: CHECK_TIMEOUT_MS })
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// The call rejects when the program cannot start or outlives its
|
|
178
|
+
// timeout; the result arrives whatever the exit code otherwise.
|
|
179
|
+
const message = errorMessage(err)
|
|
180
|
+
if (/failed to start/i.test(message)) setState('missing-cli', message)
|
|
181
|
+
else if (await lockHeld($, cwd)) retrySoon($)
|
|
182
|
+
else setState('error', message)
|
|
183
|
+
show($)
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
const parsed = parseStatus(result.stdout)
|
|
187
|
+
// An older CLI renders this project with its own engine, so its verdict
|
|
188
|
+
// says nothing about what this project's CLI would write, and its sync
|
|
189
|
+
// would drop this plugin: say so instead of offering one.
|
|
190
|
+
if (CLI_BEHIND.test((result.stderr || '') + ' ' + (result.stdout || ''))) setState('old-cli', reasonLine(result.stderr))
|
|
191
|
+
// Exit 0 is up to date and 2 is `out-of-date`, the only verdicts sync
|
|
192
|
+
// --check gives; anything else is the CLI refusing to answer.
|
|
193
|
+
else if (result.exitCode === 0) setState('ok', parsed.detail)
|
|
194
|
+
else if (result.exitCode === 2) setState('stale', parsed.detail || 'sync needed')
|
|
195
|
+
// Another holder of the project lock — someone's sync, or another
|
|
196
|
+
// session's check — is not a fault: the last verdict stays, and the check
|
|
197
|
+
// asks again shortly instead of queueing ahead of a waiting sync.
|
|
198
|
+
else if (LOCK_REFUSAL.test(result.stderr || '')) retrySoon($)
|
|
199
|
+
else setState('error', parsed.detail || reasonLine(result.stderr) || 'exit ' + result.exitCode)
|
|
200
|
+
show($)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// A check cut off at its timeout behind another command, or still finishing
|
|
204
|
+
// on its own, leaves the project lock in place: that is a command still
|
|
205
|
+
// running, not a failed check. The process the timeout abandoned keeps running.
|
|
206
|
+
async function lockHeld($, root) {
|
|
207
|
+
if (!root) return false
|
|
208
|
+
try {
|
|
209
|
+
return await $.fs.exists(root + '/.intelligence/sync.lock')
|
|
210
|
+
} catch {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function runSync($) {
|
|
216
|
+
if (state.busy) return
|
|
217
|
+
if (state.status === 'old-cli') {
|
|
218
|
+
$.ui.toast('update the intelligence CLI first: intelligence upgrade')
|
|
219
|
+
return 'not run'
|
|
220
|
+
}
|
|
221
|
+
state.busy = true
|
|
222
|
+
show($)
|
|
223
|
+
let outcome
|
|
224
|
+
let cwd = ''
|
|
225
|
+
try {
|
|
226
|
+
// A check of ours still running holds the project lock; the CLI would
|
|
227
|
+
// wait for it too, but waiting here keeps its late answer from landing
|
|
228
|
+
// after the sync's own.
|
|
229
|
+
if (pendingCheck) await pendingCheck
|
|
230
|
+
cwd = await $.session.root()
|
|
231
|
+
const result = await $.process.run(['intelligence', 'sync', '--compact'], { cwd, timeoutMs: SYNC_TIMEOUT_MS })
|
|
232
|
+
if (result.exitCode === 0) {
|
|
233
|
+
outcome = 'sync finished'
|
|
234
|
+
} else {
|
|
235
|
+
const reason = reasonLine(result.stderr) || parseStatus(result.stdout).detail || 'exit ' + result.exitCode
|
|
236
|
+
outcome = 'sync failed: ' + reason
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
// A sync past its timeout, which includes waiting for a check, keeps
|
|
240
|
+
// running and holding the lock: it has not failed yet.
|
|
241
|
+
if (!/failed to start/i.test(errorMessage(err)) && (await lockHeld($, cwd))) {
|
|
242
|
+
outcome = 'sync is still running; the status updates when it ends'
|
|
243
|
+
} else {
|
|
244
|
+
outcome = 'sync could not run: ' + errorMessage(err)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// The check, not the sync's own exit code, decides what the status shows:
|
|
248
|
+
// it is the same judgement the next timer tick would make. Until it answers
|
|
249
|
+
// the status is `checking`, so the band never offers `Sync now` again for
|
|
250
|
+
// the verdict the sync just replaced.
|
|
251
|
+
state.status = 'unknown'
|
|
252
|
+
state.detail = ''
|
|
253
|
+
state.busy = false
|
|
254
|
+
show($)
|
|
255
|
+
$.ui.toast(outcome)
|
|
256
|
+
await check($)
|
|
257
|
+
return outcome
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function register(on) {
|
|
261
|
+
on('session.start', async ($, e, next) => {
|
|
262
|
+
// A build without surfaces() draws in the terminal; the band is the default.
|
|
263
|
+
try {
|
|
264
|
+
band = (await $.session.surfaces()).some((s) => s === 'terminal' || s === 'desktop')
|
|
265
|
+
} catch {
|
|
266
|
+
band = true
|
|
267
|
+
}
|
|
268
|
+
// The label shows at once, turning, while a cold first check can take
|
|
269
|
+
// minutes.
|
|
270
|
+
show($)
|
|
271
|
+
// Neither the first check nor the timer is awaited: the session must
|
|
272
|
+
// never wait on the CLI.
|
|
273
|
+
check($)
|
|
274
|
+
$.clock.every(CHECK_EVERY_MS, () => check($))
|
|
275
|
+
// Registered last and guarded: a taken name throws, and a hook that
|
|
276
|
+
// throws is skipped from that line on.
|
|
277
|
+
try {
|
|
278
|
+
await $.command.register({
|
|
279
|
+
name: 'intelligence',
|
|
280
|
+
description: 'Show Intelligence sync status or run sync',
|
|
281
|
+
argumentHint: '[sync|check]',
|
|
282
|
+
immediate: true,
|
|
283
|
+
})
|
|
284
|
+
} catch (err) {
|
|
285
|
+
$.ui.log('/intelligence not registered: ' + errorMessage(err), { to: 'debug' })
|
|
286
|
+
}
|
|
287
|
+
return next(e)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
on('command.run', { command: 'intelligence' }, async ($, e) => {
|
|
291
|
+
const args = (e.args || '').trim()
|
|
292
|
+
if (args === 'sync') {
|
|
293
|
+
if (state.busy) return { text: 'a sync started from here is still running' }
|
|
294
|
+
const outcome = await runSync($)
|
|
295
|
+
return { text: outcome + ' — ' + statusText() }
|
|
296
|
+
}
|
|
297
|
+
if (args === 'check') {
|
|
298
|
+
await check($)
|
|
299
|
+
return { text: statusText() }
|
|
300
|
+
}
|
|
301
|
+
if (args === '') {
|
|
302
|
+
const detail = state.detail && state.status !== 'ok' ? ' (' + state.detail + ')' : ''
|
|
303
|
+
return { text: statusText() + detail }
|
|
304
|
+
}
|
|
305
|
+
return { text: 'usage: /intelligence [sync|check]' }
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// The band above the prompt, where Claude Code draws nothing of its own,
|
|
309
|
+
// carries the status while there is something to wait for or act on, and
|
|
310
|
+
// is gone while the project is up to date.
|
|
311
|
+
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
|
312
|
+
if (state.status === 'ok' && !state.busy) return next(e)
|
|
313
|
+
const { Box, Text, Button } = $.ui.resolve(e)
|
|
314
|
+
const color = state.status === 'error' || state.status === 'old-cli' ? 'red' : state.status === 'stale' ? 'yellow' : undefined
|
|
315
|
+
const children = [Text({ color, dimColor: animating(), children: [label()] })]
|
|
316
|
+
// A digit hotkey on the band also fires from an empty prompt, typed and
|
|
317
|
+
// left for a moment; a letter would need the band focused first. Plain,
|
|
318
|
+
// because only a plain button shows its hotkey in the terminal.
|
|
319
|
+
if (state.status === 'stale' && !state.busy) {
|
|
320
|
+
children.push(Button({ key: 'sync', label: 'Sync now', hotkey: '1', plain: true, onPress: () => runSync($) }))
|
|
321
|
+
}
|
|
322
|
+
return Box({ flexDirection: 'row', columnGap: 2, children })
|
|
323
|
+
})
|
|
324
|
+
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
# Skills: copy skill directories in full (SKILL.md + bundled resources),
|
|
7
7
|
# effort -> Claude's effort level
|
|
8
8
|
# Agents: tier -> model, effort -> effort, access -> tools/disallowedTools
|
|
9
|
+
# Plugin: with `targets.claude.plugin: true`, the Claude Code mod in
|
|
10
|
+
# adapters/claude-plugin/ is rendered into <output>/skills/intelligence/
|
|
11
|
+
# (decision 0022). Claude Code loads a plugin from a project's skills
|
|
12
|
+
# directory, so the plugin lives inside a path this contract already owns:
|
|
13
|
+
# ownership, Git policy, snapshots and `sync --check` need no new record.
|
|
9
14
|
#
|
|
10
15
|
# Every per-file loop batches its work into one awk process (see the batched
|
|
11
16
|
# helpers in lib/common.sh): process spawns dominate sync time on Windows.
|
|
@@ -29,6 +34,52 @@ adapter_contract_claude() {
|
|
|
29
34
|
adapter_contract_include "$output/settings.json"
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
# claude_plugin_var <manifest> — the one reader of targets.claude.plugin. Sets
|
|
38
|
+
# IS_CLAUDE_PLUGIN to 1 when the project renders the Claude Code plugin and 0
|
|
39
|
+
# for the default; returns 1 for a value target_bool_var refuses.
|
|
40
|
+
claude_plugin_var() {
|
|
41
|
+
local rc=0
|
|
42
|
+
target_bool_var "$1" claude plugin || rc=1
|
|
43
|
+
IS_CLAUDE_PLUGIN="$IS_TARGET_BOOL"
|
|
44
|
+
return "$rc"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# The directory name the plugin takes under <output>/skills: a source skill
|
|
48
|
+
# rendered under this name would be overwritten, so sync refuses the pair.
|
|
49
|
+
IS_CLAUDE_PLUGIN_NAME=intelligence
|
|
50
|
+
|
|
51
|
+
# render_claude_plugin <output_dir> — copy the plugin template byte for byte,
|
|
52
|
+
# stamp the engine version into its manifest and normalize line endings. The
|
|
53
|
+
# template is in the engine's adapter directory, never in the content package:
|
|
54
|
+
# it is executable tooling, and the bundle copies all of engine/. It is found
|
|
55
|
+
# through the engine root, so a project's own claude.sh renders it too.
|
|
56
|
+
render_claude_plugin() {
|
|
57
|
+
local output_dir="$1" tpl
|
|
58
|
+
local dst="$output_dir/skills/$IS_CLAUDE_PLUGIN_NAME"
|
|
59
|
+
# Rendered beside the destination, under a name no skill can take, and
|
|
60
|
+
# swapped in only when it differs: a running session reloads the mod on
|
|
61
|
+
# every file change, and announces each reload in its transcript.
|
|
62
|
+
local tmp="$output_dir/skills/.$IS_CLAUDE_PLUGIN_NAME.render.$$"
|
|
63
|
+
engine_root_var
|
|
64
|
+
tpl="$IS_ENGINE_ROOT/adapters/claude-plugin"
|
|
65
|
+
engine_version_var
|
|
66
|
+
rm -rf "$tmp"
|
|
67
|
+
mkdir -p "$tmp/.claude-plugin" "$tmp/hooks"
|
|
68
|
+
cp "$tpl/hooks/hooks.json" "$tpl/hooks/register.js" "$tmp/hooks/"
|
|
69
|
+
cp "$tpl/README.md" "$tmp/README.md"
|
|
70
|
+
awk -v v="$IS_ENGINE_VERSION" '{ sub(/__INTELLIGENCE_VERSION__/, v); print }' \
|
|
71
|
+
"$tpl/.claude-plugin/plugin.json" > "$tmp/.claude-plugin/plugin.json"
|
|
72
|
+
finalize_output_files "$tmp/.claude-plugin/plugin.json" "$tmp/hooks/hooks.json" \
|
|
73
|
+
"$tmp/hooks/register.js" "$tmp/README.md"
|
|
74
|
+
if [ -d "$dst" ] && [ ! -L "$dst" ] && diff -r -q "$tmp" "$dst" >/dev/null 2>&1; then
|
|
75
|
+
rm -rf "$tmp"
|
|
76
|
+
else
|
|
77
|
+
rm -rf "$dst"
|
|
78
|
+
mv "$tmp" "$dst"
|
|
79
|
+
fi
|
|
80
|
+
echo " plugin: $IS_CLAUDE_PLUGIN_NAME (Claude Code mod $IS_ENGINE_VERSION)"
|
|
81
|
+
}
|
|
82
|
+
|
|
32
83
|
# Sync rules to Claude format (copy as-is, normalize LF)
|
|
33
84
|
sync_claude_rules() {
|
|
34
85
|
local repo_root="$1"
|
|
@@ -53,13 +104,32 @@ sync_claude_skills() {
|
|
|
53
104
|
local config_file="$2"
|
|
54
105
|
local output_dir="$3"
|
|
55
106
|
|
|
56
|
-
local i name
|
|
107
|
+
local i name clash="" nocase=0
|
|
57
108
|
local -a skill_dirs=()
|
|
58
109
|
# The catalog's skills are the ones the inventory renders, symlinked ones
|
|
59
110
|
# included (decision 0017).
|
|
60
111
|
output_rel_var "$repo_root" "$output_dir"
|
|
61
112
|
source_artifacts_var "$repo_root" "$config_file" skills claude "$IS_OUTPUT_REL/skills/{}"
|
|
62
113
|
[ "${#IS_SOURCE_FILES[@]}" -gt 0 ] || return 0
|
|
114
|
+
# The plugin takes one skill directory name; a skill rendered under it and
|
|
115
|
+
# the plugin would each erase the other, so neither is written. Names
|
|
116
|
+
# compare without case: on Windows and macOS `Intelligence/` is the same
|
|
117
|
+
# directory. (nocasematch, not ${name,,}: macOS ships bash 3.2.)
|
|
118
|
+
if [ "${IS_CLAUDE_PLUGIN:-0}" = 1 ]; then
|
|
119
|
+
shopt -q nocasematch && nocase=1
|
|
120
|
+
shopt -s nocasematch
|
|
121
|
+
for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
|
|
122
|
+
if [[ "${IS_SOURCE_NAMES[i]}" == "$IS_CLAUDE_PLUGIN_NAME" ]]; then
|
|
123
|
+
clash="${IS_SOURCE_FILES[i]#"$repo_root/"}"
|
|
124
|
+
break
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
[ "$nocase" = 1 ] || shopt -u nocasematch
|
|
128
|
+
if [ -n "$clash" ]; then
|
|
129
|
+
echo "ERROR: skill $clash renders as $IS_OUTPUT_REL/skills/$IS_CLAUDE_PLUGIN_NAME, the directory targets.claude.plugin: true reserves for the Claude Code plugin. Rename the skill or set targets.claude.plugin to false." >&2
|
|
130
|
+
return 1
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
63
133
|
for ((i = 0; i < ${#IS_SOURCE_FILES[@]}; i++)); do
|
|
64
134
|
skill_dirs+=("${IS_SOURCE_FILES[i]%/SKILL.md}/")
|
|
65
135
|
done
|
|
@@ -156,18 +226,39 @@ sync_to_claude() {
|
|
|
156
226
|
|
|
157
227
|
echo "=== Claude Code ==="
|
|
158
228
|
|
|
229
|
+
# The flag is read before anything is removed: a malformed value refuses
|
|
230
|
+
# the sync with the reason, and the output stays exactly as it was.
|
|
231
|
+
claude_plugin_var "$config_file" || {
|
|
232
|
+
echo "ERROR: targets.claude.plugin must be true or false: true renders the Claude Code plugin into ${output_dir#"$repo_root/"}/skills/$IS_CLAUDE_PLUGIN_NAME, false or omission does not." >&2
|
|
233
|
+
return 1
|
|
234
|
+
}
|
|
235
|
+
|
|
159
236
|
# Clean generated content only — preserve any project-level files
|
|
160
237
|
# the user manages directly (settings.json, settings.local.json,
|
|
161
238
|
# commands/, statusline.sh, etc.).
|
|
162
239
|
rm -rf "$output_dir/rules" "$output_dir/agents"
|
|
163
240
|
if [ -d "$output_dir/skills" ]; then
|
|
164
|
-
|
|
241
|
+
# The plugin directory stays while the flag holds: Claude Code reloads
|
|
242
|
+
# a mod whose files change, so render_claude_plugin rewrites it only
|
|
243
|
+
# when its content differs. A skill of that name was refused above.
|
|
244
|
+
if [ "$IS_CLAUDE_PLUGIN" = 1 ]; then
|
|
245
|
+
find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) \
|
|
246
|
+
! -name "$IS_CLAUDE_PLUGIN_NAME" -exec rm -rf {} +
|
|
247
|
+
else
|
|
248
|
+
find "$output_dir/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec rm -rf {} +
|
|
249
|
+
fi
|
|
165
250
|
fi
|
|
166
251
|
mkdir -p "$output_dir/rules" "$output_dir/skills" "$output_dir/agents"
|
|
167
252
|
|
|
168
253
|
sync_claude_rules "$repo_root" "$config_file" "$output_dir"
|
|
254
|
+
# Called plainly, never on the left of `||`: that would turn errexit off
|
|
255
|
+
# for every copy inside, and a failed one would no longer fail the sync.
|
|
169
256
|
sync_claude_skills "$repo_root" "$config_file" "$output_dir"
|
|
170
257
|
sync_claude_agents "$repo_root" "$config_file" "$output_dir"
|
|
258
|
+
# The skills renderer has refused a skill of the plugin's name by now, so
|
|
259
|
+
# the plugin never lands on top of one. Cleared above with every other
|
|
260
|
+
# skill directory, it exists only while the flag says so.
|
|
261
|
+
[ "$IS_CLAUDE_PLUGIN" = 0 ] || render_claude_plugin "$output_dir"
|
|
171
262
|
|
|
172
263
|
count_matching_files "$output_dir/rules" "*.md" \
|
|
173
264
|
"$output_dir/skills" "SKILL.md" "$output_dir/agents" "*.md"
|
|
@@ -55,19 +55,13 @@ adapter_contract_copilot() {
|
|
|
55
55
|
|
|
56
56
|
# copilot_commit_output_var <manifest> — the one reader of
|
|
57
57
|
# targets.copilot.commit_output. Sets IS_COPILOT_COMMIT_OUTPUT to 1 when the
|
|
58
|
-
# project commits generated output and 0 for the default; returns 1 for
|
|
59
|
-
#
|
|
60
|
-
# only — is the default.
|
|
58
|
+
# project commits generated output and 0 for the default; returns 1 for a value
|
|
59
|
+
# target_bool_var refuses.
|
|
61
60
|
copilot_commit_output_var() {
|
|
62
|
-
local
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
case "$setting" in
|
|
67
|
-
''|false) ;;
|
|
68
|
-
true) IS_COPILOT_COMMIT_OUTPUT=1 ;;
|
|
69
|
-
*) return 1 ;;
|
|
70
|
-
esac
|
|
61
|
+
local rc=0
|
|
62
|
+
target_bool_var "$1" copilot commit_output || rc=1
|
|
63
|
+
IS_COPILOT_COMMIT_OUTPUT="$IS_TARGET_BOOL"
|
|
64
|
+
return "$rc"
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
# copilot_paths_rows <file>... — batch form of the old per-file `paths:`
|
package/engine/lib/common.sh
CHANGED
|
@@ -3629,6 +3629,24 @@ is_target_enabled() {
|
|
|
3629
3629
|
' "$file"
|
|
3630
3630
|
}
|
|
3631
3631
|
|
|
3632
|
+
# target_bool_var <manifest> <target> <field> — the one reading of a boolean
|
|
3633
|
+
# target field (targets.copilot.commit_output, targets.claude.plugin). Sets
|
|
3634
|
+
# IS_TARGET_BOOL to 1 for true and 0 for false, omission or an empty value;
|
|
3635
|
+
# returns 1 for anything else, so a typo never decides silently. No manifest —
|
|
3636
|
+
# a caller reading ownership only — is the default.
|
|
3637
|
+
# shellcheck disable=SC2034 # IS_TARGET_BOOL is the return channel
|
|
3638
|
+
target_bool_var() {
|
|
3639
|
+
local setting
|
|
3640
|
+
IS_TARGET_BOOL=0
|
|
3641
|
+
[ -n "$1" ] && [ -f "$1" ] || return 0
|
|
3642
|
+
setting="$(get_target_field "$1" "$2" "$3")"
|
|
3643
|
+
case "$setting" in
|
|
3644
|
+
''|false) ;;
|
|
3645
|
+
true) IS_TARGET_BOOL=1 ;;
|
|
3646
|
+
*) return 1 ;;
|
|
3647
|
+
esac
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3632
3650
|
# Get an arbitrary field from a target's config block.
|
|
3633
3651
|
# Handles both inline (`claude: { enabled: true, output: ".claude" }`)
|
|
3634
3652
|
# and block form. Uses POSIX awk only — no gawk-specific 3-arg match().
|
package/engine/lib/contract.sh
CHANGED
|
@@ -100,18 +100,27 @@ is_status() {
|
|
|
100
100
|
fi
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
# engine_root_var — set IS_ENGINE_ROOT to the engine directory, one level above
|
|
104
|
+
# this lib. A project adapter that overrides a built-in finds the engine's own
|
|
105
|
+
# files through it, never beside itself.
|
|
106
|
+
engine_root_var() {
|
|
107
|
+
local lib="${BASH_SOURCE[0]}"
|
|
108
|
+
case "$lib" in
|
|
109
|
+
*/*) lib="${lib%/*}" ;;
|
|
110
|
+
*) lib="." ;;
|
|
111
|
+
esac
|
|
112
|
+
IS_ENGINE_ROOT="$lib/.."
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
# Engine version = VERSION one level above this lib (BASH_SOURCE works when
|
|
104
116
|
# sourced). Empty if unreadable — callers treat empty as "no guard".
|
|
105
117
|
# engine_version_var sets IS_ENGINE_VERSION, and IS_ENGINE_VERSION_FOUND=1 when
|
|
106
118
|
# the file exists; it reads the file once per process.
|
|
107
119
|
engine_version_var() {
|
|
108
120
|
[ -z "${IS_ENGINE_VERSION_FOUND:-}" ] || return 0
|
|
109
|
-
local
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*) lib="." ;;
|
|
113
|
-
esac
|
|
114
|
-
vf="$lib/../VERSION"
|
|
121
|
+
local vf content=""
|
|
122
|
+
engine_root_var
|
|
123
|
+
vf="$IS_ENGINE_ROOT/VERSION"
|
|
115
124
|
IS_ENGINE_VERSION=""
|
|
116
125
|
IS_ENGINE_VERSION_FOUND=0
|
|
117
126
|
[ -f "$vf" ] || return 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainova-systems/intelligence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Build, version and distribute AI agent intelligence across your organization — one CLI, versioned Intelligence Packages, and a sync engine for Claude Code, Cursor, Copilot, Codex, Pi and OpenCode.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"intelligence": "bin/intelligence.js"
|
|
@@ -129,6 +129,17 @@ Code ignore theirs, and never `.github/` itself. `targets.copilot.commit_output:
|
|
|
129
129
|
true` keeps them tracked for Copilot on github.com; the setting is defined in
|
|
130
130
|
[Generated output and version control](conventions.md#generated-output-and-version-control).
|
|
131
131
|
|
|
132
|
+
The Claude adapter renders a Claude Code plugin into `<output>/skills/intelligence/`
|
|
133
|
+
when `targets.claude.plugin` is `true` (decision 0022): a mod that checks
|
|
134
|
+
`intelligence sync --check` at session start and every five minutes, shows the
|
|
135
|
+
verdict in a line above the prompt and runs sync from a button there or
|
|
136
|
+
`/intelligence sync`.
|
|
137
|
+
The template is `engine/adapters/claude-plugin/`, copied with the engine version
|
|
138
|
+
stamped into `plugin.json`. It lives inside the owned `skills` path, so it needs
|
|
139
|
+
no contract record of its own. The field's values, the name collision it refuses
|
|
140
|
+
and the plugin's limits are defined in the
|
|
141
|
+
[CLI reference](https://github.com/ainova-systems/intelligence/blob/main/docs/cli.md#intelligence-sync).
|
|
142
|
+
|
|
132
143
|
The engine calls:
|
|
133
144
|
|
|
134
145
|
```text
|
|
@@ -373,7 +384,7 @@ For a built-in adapter, add the same assertions to the repository's smoke or lif
|
|
|
373
384
|
|---|---|
|
|
374
385
|
| `agents` | `AGENTS.md` |
|
|
375
386
|
| `antigravity` | `.agents/rules`, `.agents/agents`, `.agents/skills` |
|
|
376
|
-
| `claude` | `.claude/rules`, `.claude/agents`, `.claude/skills` |
|
|
387
|
+
| `claude` | `.claude/rules`, `.claude/agents`, `.claude/skills` (with `plugin: true`, `.claude/skills/intelligence` is the Claude Code plugin) |
|
|
377
388
|
| `cursor` | `.cursor/rules`, `.cursor/agents`, `.cursor/skills` |
|
|
378
389
|
| `copilot` | `.github/instructions`, `.github/agents`, `.github/skills` |
|
|
379
390
|
| `codex` | `.codex/agents`, `.agents/skills`, marker-owned `AGENTS.override.md` |
|