@alexandrealvaro/agentic 0.9.3-beta.1 → 0.9.4-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/init.js +1 -0
- package/src/commands/update.js +43 -14
- package/src/lib/install.js +10 -1
- package/src/lib/rootdoc.js +10 -3
- package/src/lib/state.js +13 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
package/src/commands/update.js
CHANGED
|
@@ -47,6 +47,7 @@ const ACTION_SYMBOL = {
|
|
|
47
47
|
kept: '·',
|
|
48
48
|
skipped: '!',
|
|
49
49
|
removed: '-',
|
|
50
|
+
'removed-missing': '?',
|
|
50
51
|
'orphan-kept': '?',
|
|
51
52
|
};
|
|
52
53
|
|
|
@@ -55,6 +56,7 @@ const ROOT_DOC_LABEL = {
|
|
|
55
56
|
updated: '~ ',
|
|
56
57
|
unchanged: '· ',
|
|
57
58
|
skipped: '! ',
|
|
59
|
+
'kept-stale': '! ',
|
|
58
60
|
absent: '',
|
|
59
61
|
};
|
|
60
62
|
|
|
@@ -86,13 +88,24 @@ function skillsForAgent(agent, profileName, optedSkills) {
|
|
|
86
88
|
return [...universal, ...conditional];
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Load every per-agent state file once. Returns `{ statesByAgent, agents }`
|
|
93
|
+
* where `agents` lists the agents whose state files exist on disk. Avoids
|
|
94
|
+
* the prior pattern of calling `loadState` twice per agent (once for
|
|
95
|
+
* presence detection, once for content); also surfaces malformed-state
|
|
96
|
+
* errors with the loader's own context, in a single pass.
|
|
97
|
+
*/
|
|
98
|
+
function loadStatesOnce(cwd) {
|
|
99
|
+
const statesByAgent = {};
|
|
100
|
+
const agents = [];
|
|
91
101
|
for (const agent of VALID_AGENTS) {
|
|
92
102
|
const state = loadState(cwd, agent);
|
|
93
|
-
if (state)
|
|
103
|
+
if (state) {
|
|
104
|
+
statesByAgent[agent] = state;
|
|
105
|
+
agents.push(agent);
|
|
106
|
+
}
|
|
94
107
|
}
|
|
95
|
-
return
|
|
108
|
+
return { statesByAgent, agents };
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
function previouslyOptedConditional(previousStates, currentAgents, profileName) {
|
|
@@ -140,18 +153,23 @@ export async function updateCommand(opts) {
|
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
const cwd = process.cwd();
|
|
143
|
-
|
|
156
|
+
// `--agent` is purely a narrowing flag and does not imply non-interactive
|
|
157
|
+
// intent. Only `--yes` or a non-TTY shell suppress the TUI per ADR-0009.
|
|
158
|
+
const interactive = process.stdout.isTTY && !opts.yes;
|
|
144
159
|
const dryRun = Boolean(opts.dryRun);
|
|
145
160
|
const force = Boolean(opts.force);
|
|
146
161
|
|
|
147
162
|
const detectedAgents = detectAgents(cwd);
|
|
148
163
|
const features = detectFeatures(cwd);
|
|
149
|
-
const previousAgents =
|
|
164
|
+
const { statesByAgent, agents: previousAgents } = loadStatesOnce(cwd);
|
|
150
165
|
|
|
151
166
|
const agents = resolveAgents(opts.agent, detectedAgents, previousAgents);
|
|
167
|
+
// previousStates is the per-agent slice of statesByAgent restricted to the
|
|
168
|
+
// agents the current invocation targets. Agents outside the slice keep
|
|
169
|
+
// their state file untouched on disk.
|
|
152
170
|
const previousStates = {};
|
|
153
171
|
for (const agent of agents) {
|
|
154
|
-
previousStates[agent] =
|
|
172
|
+
previousStates[agent] = statesByAgent[agent] ?? null;
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
const profileName = profileFromStates(previousStates, agents);
|
|
@@ -282,13 +300,24 @@ export async function updateCommand(opts) {
|
|
|
282
300
|
}
|
|
283
301
|
: async () => true;
|
|
284
302
|
|
|
285
|
-
const
|
|
286
|
-
?
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
303
|
+
const confirmRootDocReplace = interactive
|
|
304
|
+
? async (path) => {
|
|
305
|
+
const answer = await p.confirm({
|
|
306
|
+
message: `${path}: managed section diverged on disk. Regenerate it? (any edits between the agentic-managed-skills markers will be lost)`,
|
|
307
|
+
initialValue: false,
|
|
308
|
+
});
|
|
309
|
+
if (p.isCancel(answer)) return false;
|
|
310
|
+
return answer;
|
|
311
|
+
}
|
|
312
|
+
: async () => Boolean(force);
|
|
313
|
+
|
|
314
|
+
const rootDocAction = await updateRootDoc({
|
|
315
|
+
cwd,
|
|
316
|
+
skills: skillDisplayOrder,
|
|
317
|
+
confirmAppend,
|
|
318
|
+
confirmReplace: confirmRootDocReplace,
|
|
319
|
+
dryRun,
|
|
320
|
+
});
|
|
292
321
|
|
|
293
322
|
const lines = allActions.map((a) => {
|
|
294
323
|
const sym = ACTION_SYMBOL[a.type] ?? '?';
|
package/src/lib/install.js
CHANGED
|
@@ -356,7 +356,16 @@ export async function removeOrphanSkills({
|
|
|
356
356
|
|
|
357
357
|
for (const f of entry.files) {
|
|
358
358
|
const abs = join(cwd, f.path);
|
|
359
|
-
if (existsSync(abs)
|
|
359
|
+
if (!existsSync(abs)) {
|
|
360
|
+
// State recorded the file at install time but it is gone on disk
|
|
361
|
+
// now (manually deleted, moved, or never written). Surface as a
|
|
362
|
+
// distinct action so the user sees the state-vs-reality mismatch
|
|
363
|
+
// instead of a silent "removed" line for a file that was never
|
|
364
|
+
// there.
|
|
365
|
+
actions.push({ type: 'removed-missing', path: f.path, agent });
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (!dryRun) {
|
|
360
369
|
unlinkSync(abs);
|
|
361
370
|
}
|
|
362
371
|
actions.push({ type: 'removed', path: f.path, agent });
|
package/src/lib/rootdoc.js
CHANGED
|
@@ -93,18 +93,23 @@ function replaceSection(body, newSection, bounds) {
|
|
|
93
93
|
* Behaviour:
|
|
94
94
|
* - No root doc → returns { type: 'absent', path: null }. Skill bootstraps create the file later.
|
|
95
95
|
* - Section already present and matches → { type: 'unchanged', path }.
|
|
96
|
-
* - Section already present and stale →
|
|
96
|
+
* - Section already present and stale → confirmReplace(path) is called; on true → updated → { type: 'updated', path }; on false → { type: 'kept-stale', path } (managed-section diverged on disk and the user chose to keep it).
|
|
97
97
|
* - Section absent → confirmAppend(path) is called; on true → appended → { type: 'appended', path }; on false → { type: 'skipped', path }.
|
|
98
|
+
* - dryRun true → no writes; returned `type` reflects what *would* happen.
|
|
98
99
|
*
|
|
99
100
|
* @param {object} opts
|
|
100
101
|
* @param {string} opts.cwd Target project root.
|
|
101
102
|
* @param {string[]} opts.skills Skill names actually installed (in display order).
|
|
102
103
|
* @param {(path: string) => Promise<boolean>} [opts.confirmAppend] Async confirmation callback for the append-to-existing-file case. Default: skip.
|
|
104
|
+
* @param {(path: string) => Promise<boolean>} [opts.confirmReplace] Async confirmation callback for the replace-existing-section case (managed section present on disk but stale). Default: replace (preserves the v0.5 behaviour where update silently regenerates the managed block on every kit / skill change). Wire to a prompt to honor user edits between markers.
|
|
105
|
+
* @param {boolean} [opts.dryRun] When true, computes the action without writing.
|
|
103
106
|
*/
|
|
104
107
|
export async function updateRootDoc({
|
|
105
108
|
cwd,
|
|
106
109
|
skills,
|
|
107
110
|
confirmAppend = async () => false,
|
|
111
|
+
confirmReplace = async () => true,
|
|
112
|
+
dryRun = false,
|
|
108
113
|
}) {
|
|
109
114
|
const found = findRootDoc(cwd);
|
|
110
115
|
if (!found) return { type: 'absent', path: null };
|
|
@@ -116,7 +121,9 @@ export async function updateRootDoc({
|
|
|
116
121
|
if (bounds) {
|
|
117
122
|
const updated = replaceSection(body, section, bounds);
|
|
118
123
|
if (updated === body) return { type: 'unchanged', path: found.name };
|
|
119
|
-
|
|
124
|
+
const ok = await confirmReplace(found.name);
|
|
125
|
+
if (!ok) return { type: 'kept-stale', path: found.name };
|
|
126
|
+
if (!dryRun) writeFileSync(found.path, updated);
|
|
120
127
|
return { type: 'updated', path: found.name };
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -124,6 +131,6 @@ export async function updateRootDoc({
|
|
|
124
131
|
if (!ok) return { type: 'skipped', path: found.name };
|
|
125
132
|
|
|
126
133
|
const sep = body.endsWith('\n') ? '\n' : '\n\n';
|
|
127
|
-
writeFileSync(found.path, body + sep + section + '\n');
|
|
134
|
+
if (!dryRun) writeFileSync(found.path, body + sep + section + '\n');
|
|
128
135
|
return { type: 'appended', path: found.name };
|
|
129
136
|
}
|
package/src/lib/state.js
CHANGED
|
@@ -77,9 +77,22 @@ export function saveState(cwd, agent, state) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function orderState(state) {
|
|
80
|
+
if (!state || typeof state !== 'object') {
|
|
81
|
+
throw new Error('orderState: state must be an object');
|
|
82
|
+
}
|
|
83
|
+
if (!state.skills || typeof state.skills !== 'object') {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'orderState: state.skills must be an object (got ' + typeof state.skills + ')'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
80
88
|
const skills = {};
|
|
81
89
|
for (const skillName of Object.keys(state.skills).sort()) {
|
|
82
90
|
const entry = state.skills[skillName];
|
|
91
|
+
if (!entry || !Array.isArray(entry.files)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`orderState: state.skills["${skillName}"].files must be an array`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
83
96
|
const files = [...entry.files].sort((a, b) => a.path.localeCompare(b.path));
|
|
84
97
|
skills[skillName] = {
|
|
85
98
|
version: entry.version,
|