octwin-cli 0.8.11 → 0.8.13

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.
@@ -58,6 +58,11 @@ export function loadPrimitiveArgSpecs(packDir) {
58
58
  required: Array.isArray(schema.required) ? schema.required : [],
59
59
  open: ap !== undefined && ap !== false,
60
60
  requiresOneOf: Array.isArray(entry.requiresOneOf) ? entry.requiresOneOf : [],
61
+ // Kept only when BOTH halves are present. A pair with no message would print a warning
62
+ // that names two argument keys and says nothing about why they are a trap — which is the
63
+ // sort of warning authors learn to scroll past.
64
+ warnsTogether: (entry.warnsTogether ?? [])
65
+ .filter((w) => Array.isArray(w?.args) && w.args.length > 0 && typeof w?.message === 'string' && w.message.length > 0),
61
66
  });
62
67
  }
63
68
  }
@@ -74,6 +79,10 @@ export function loadPrimitiveArgSpecs(packDir) {
74
79
  * primitive's declared set. Walks the whole document rather than just `steps:`
75
80
  * — a `do:` is legal in a `foreach` body, a `collect` hook, an `outputs:` port
76
81
  * body and `require:`, and a dropped argument is just as invisible there.
82
+ *
83
+ * Warnings come back on their own list rather than mixed into `ArgFinding`. They have a different
84
+ * severity, a different exit code, and a different sentence — folding them together would have
85
+ * meant either failing a build over a warning or hiding an error inside a warning count.
77
86
  */
78
87
  export function findArgViolations(doc, file, specs) {
79
88
  const findings = [];
@@ -107,6 +116,52 @@ export function findArgViolations(doc, file, specs) {
107
116
  walk(doc, []);
108
117
  return findings;
109
118
  }
119
+ /**
120
+ * Walk for `do:` nodes whose `args:` supply every member of a declared `warnsTogether` pair.
121
+ *
122
+ * A separate walk rather than a second output of `findArgViolations`, on purpose: that function
123
+ * skips a primitive whose schema is OPEN (`!spec.open`), because an open schema makes "unknown
124
+ * key" unanswerable. A `warnsTogether` pair is answerable either way — it names two keys that ARE
125
+ * declared — so gating it on closedness would silently drop the check for every passthrough
126
+ * primitive, which is exactly the shape of hole a scan grows without anyone noticing.
127
+ */
128
+ export function findArgWarnings(doc, file, specs) {
129
+ const warnings = [];
130
+ const walk = (node, path) => {
131
+ if (Array.isArray(node)) {
132
+ node.forEach((v, i) => walk(v, [...path, i]));
133
+ return;
134
+ }
135
+ if (!node || typeof node !== 'object')
136
+ return;
137
+ const obj = node;
138
+ if (typeof obj.do === 'string') {
139
+ const spec = specs.get(obj.do);
140
+ if (spec?.warnsTogether.length) {
141
+ const args = (obj.args && typeof obj.args === 'object' && !Array.isArray(obj.args))
142
+ ? obj.args
143
+ : {};
144
+ for (const pair of spec.warnsTogether) {
145
+ // KEY PRESENCE only, matching the platform's own `checkPrimitiveArgs`. The values are
146
+ // almost always expressions (`record_id: '$found.record_id'`), so judging them would
147
+ // mean judging a string — and the trap is about the combination, not the values.
148
+ if (pair.args.every(k => k in args)) {
149
+ warnings.push({ file, primitive: obj.do, path: [...path], args: [...pair.args], message: pair.message });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ for (const [k, v] of Object.entries(obj))
155
+ walk(v, [...path, k]);
156
+ };
157
+ walk(doc, []);
158
+ return warnings;
159
+ }
160
+ /** One line per warning: where, which pair, and the primitive's own reason. */
161
+ export function describeArgWarning(w) {
162
+ const at = w.line ? `${w.file}:${w.line}` : w.file;
163
+ return `${at}: \`${w.primitive}\` passes ${w.args.map(a => `'${a}'`).join(' + ')} together — ${w.message}`;
164
+ }
110
165
  /** One-line human message per finding. */
111
166
  export function describeArgFinding(f) {
112
167
  const parts = [];
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Who sent a render on the chat stream — and whether it answers the turn.
3
+ *
4
+ * `octwin chat` reads ONE stream for a conversation, and more than the AI Agent writes to it:
5
+ *
6
+ * - a `notify` send — a `schedule_notify` reminder, a reaction's notification, a queued
7
+ * briefing — which answers nothing the author just sent (`source: 'notify'` on the web
8
+ * envelope);
9
+ * - a person holding the conversation in Live chat (`author.kind: 'human'`, with a name);
10
+ * - a platform line such as a handoff's join / return copy (`author.kind: 'system'`).
11
+ *
12
+ * Every one of them used to print as `← <body>`, exactly like the AI Agent's reply. The case
13
+ * that cost an author real time (chughtai, 2026-09-23): a reminder fired mid `--script`, the CLI
14
+ * took it for the turn's reply and ended the turn on it, and the real reply — which `octwin logs`
15
+ * showed the flow had produced correctly — looked lost.
16
+ *
17
+ * Kept free of I/O so the rules can be tested without a stream.
18
+ */
19
+ export function chatSender(ev) {
20
+ if (ev.source === 'notify')
21
+ return 'notify';
22
+ if (ev.author?.kind === 'human')
23
+ return 'operator';
24
+ if (ev.author?.kind === 'system')
25
+ return 'platform';
26
+ return 'agent';
27
+ }
28
+ /** The label printed before the body — empty for the AI Agent, whose replies stay `← …`. */
29
+ export function chatLabel(ev) {
30
+ switch (chatSender(ev)) {
31
+ case 'notify': return '[notify] ';
32
+ case 'operator': return ev.author?.name ? `[operator: ${ev.author.name}] ` : '[operator] ';
33
+ case 'platform': return '[platform] ';
34
+ default: return '';
35
+ }
36
+ }
37
+ /**
38
+ * Whether this render is an ANSWER to the message the author just sent — what ends a turn's wait.
39
+ * A notify is not: it would have been sent whether or not the author wrote anything. An operator's
40
+ * line and a platform line are: in a held conversation they ARE the reply.
41
+ */
42
+ export function answersTheTurn(ev) {
43
+ return chatSender(ev) !== 'notify';
44
+ }
@@ -56,6 +56,17 @@ export function reportChecks(checks, io) {
56
56
  io.die(outcome.hint);
57
57
  }
58
58
  // eslint-disable-next-line no-fallthrough -- `die` returns never; the case above cannot exit.
59
+ case 'warnings': {
60
+ // On `log`, not `err`, and the run CONTINUES. A warning that stopped the run, or that
61
+ // arrived on the error stream, would be an error wearing a different word — and the next
62
+ // check is the one the author was actually waiting for.
63
+ const n = outcome.lines.length;
64
+ io.log(`⚠ ${n} ${outcome.noun}${n === 1 ? '' : 's'} — not fatal:`);
65
+ for (const line of outcome.lines)
66
+ io.log(` ⚠ ${line}`);
67
+ io.log(` ${outcome.hint}`);
68
+ break;
69
+ }
59
70
  case 'not-run':
60
71
  notRun.push({ label: check.label, lookup: outcome.lookup });
61
72
  break;
@@ -9,7 +9,7 @@
9
9
  * platform's own `approve_apply` is the common case: `confirm_title` misspelled
10
10
  * silently falls back to the hardcoded Arabic default.
11
11
  *
12
- * PACK TEMPLATES SHADOW PLATFORM ONES. A pack may ship `templates/<name>.yaml`
12
+ * PACK TEMPLATES SHADOW PLATFORM ONES. A pack may ship `templates/<name>.template.yaml`
13
13
  * of its own, and a pack template of the same name wins. So the caller passes
14
14
  * the pack's own template names and this check stays silent about them entirely
15
15
  * — it has no schema for a template it did not publish, and inventing one would
@@ -96,7 +96,9 @@ export function describeTemplateFinding(f) {
96
96
  switch (f.kind) {
97
97
  case 'unknown':
98
98
  return `${f.file}: \`use: ${f.template}\` — no such template. Platform templates: ${f.known.join(', ')}`
99
- + `. (A template of your own must live at templates/${f.template}.yaml in the pack.)`;
99
+ // `.template.yaml`, not `.yaml`: the platform loads only that suffix, so a template saved
100
+ // as this hint used to say was silently ignored and the `use:` stayed unresolved.
101
+ + `. (A template of your own must live at templates/${f.template}.template.yaml in the pack.)`;
100
102
  case 'unknown-param':
101
103
  return `${f.file}: \`use: ${f.template}\` passes ${f.keys.map(k => `'${k}'`).join(', ')}, which the template declares no param for `
102
104
  + `— dropped in silence, and the body sees undefined. Params: ${f.params.join(', ')}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octwin-cli",
3
- "version": "0.8.11",
3
+ "version": "0.8.13",
4
4
  "description": "Octwin external-pack developer CLI (by CEQUENS) — scaffold, validate, deploy, and check pure-YAML packs on your tenant.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,6 +41,6 @@
41
41
  "yaml"
42
42
  ],
43
43
  "author": "CEQUENS",
44
- "homepage": "https://www.npmjs.com/package/octwin-cli",
44
+ "homepage": "https://octwin.ai",
45
45
  "license": "MIT"
46
46
  }
@@ -1,6 +1,13 @@
1
+ # EXAMPLE — replace it or delete it; do not leave it behind.
1
2
  # An example capability the home hub invokes. Replace this with your real tool
2
3
  # (record_list + a carousel, a collect: intake, a case_open, …). Kept trivial here
3
4
  # so the scaffold is chattable end-to-end: home menu → tap "browse" → this renders.
5
+ #
6
+ # If you do not keep it, delete THREE things together: this file, every
7
+ # `browse.locale.<lang>.yaml` beside it, and the `browse` line in manifest.yaml
8
+ # `flows:` (plus the home menu row that invokes it). A file left here with its
9
+ # manifest line removed never loads — but its copy is still checked, so a pack that
10
+ # adds a second language fails validation over a flow nobody uses.
4
11
  flow_id: browse
5
12
  version: 1
6
13
  description: An example capability, reached from the home menu. Replace with your real tool.
@@ -1,3 +1,4 @@
1
+ # EXAMPLE copy for the example `browse` flow — delete it together with browse.flow.yaml.
1
2
  flow_id: browse
2
3
 
3
4
  strings:
@@ -33,7 +33,7 @@ default_settings:
33
33
  # front door; add each new tool here as you build it.
34
34
  flows:
35
35
  - home # the menu / navigation hub (see references/ux-patterns.md)
36
- - browse # an example tool the hub invokes — replace with your real capability
36
+ - browse # an EXAMPLE tool the hub invokes — replace it, or delete this line AND flows/tools/browse.*
37
37
 
38
38
  # One agent. The platform's universal agent protocol is auto-appended by
39
39
  # `createPackAgents` — `instructions:` carries only pack-supplied parts.