@lazyingart/agintiflow 0.20.217 → 0.20.218

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.
@@ -267,3 +267,66 @@ platform-aware POSIX and Windows command quoting; and recognition of bounded
267
267
  stdin `sed -n` pipelines as read-only inspection. Production code contains no
268
268
  campaign scenario IDs, target paths, expected prose, commit IDs, or acceptance
269
269
  literals.
270
+
271
+ ### Live inbox requirement coalescing
272
+
273
+ The next installed-release run tested actual mid-turn input rather than a
274
+ sequence of explicit resume commands. A DeepSeek task started from a normal
275
+ repository-writing request. While its first model call was active, two
276
+ independent corrections were appended through `aginti queue`. Both received
277
+ distinct durable inbox IDs and were applied exactly once at the first safe tool
278
+ boundary, before the source note was read.
279
+
280
+ AgInTiFlow `0.20.217` retained both corrections through later context
281
+ compaction, produced one concise artifact, removed every superseded value,
282
+ preserved the source note, added only narrowly scoped runtime ignore rules,
283
+ committed the intentional files, and left the target repository clean. An
284
+ external semantic and repository-state contract passed at target commit
285
+ `f19ff4b`. No AgInTiFlow source change or task-specific prompt branch was
286
+ needed; the run validates the generic inbox, context, evidence, and completion
287
+ contracts in the published package.
288
+
289
+ ### LabCanvas least-privilege worker integration
290
+
291
+ `labcanvas-worker-permission-018` validated the installed AgInTiFlow `0.20.217`
292
+ through LabCanvas after a real WeCom research task paused at
293
+ `permission_required`. The defect was in the integration boundary: LabCanvas
294
+ unconditionally selected AgInTi's blocked package-install tier, which also
295
+ blocked ordinary workspace shell execution. LabCanvas now keeps response-only
296
+ roles blocked while granting genuine worker roles reversible package setup
297
+ inside the Docker workspace. Host access, credentials, payment, publication,
298
+ destructive operations, and other irreversible actions remain separately
299
+ guarded.
300
+
301
+ A direct DeepSeek-backed worker smoke created and validated a bounded workspace
302
+ artifact without a permission prompt. Focused backend regressions, all WeChat
303
+ self-checks, the complete 1,442-test LabCanvas suite, and GitHub Actions passed.
304
+ The originally blocked exact-chat task was then resumed without duplicate model
305
+ work, produced a visually inspected nine-page XeLaTeX research report, and
306
+ delivered one concise message plus the PDF through the verified WeCom transport.
307
+ The follow-up message was preserved in the same chat session. The reusable fix
308
+ is AgenticApp commit `e705bb0`; no AgInTiFlow release change was required.
309
+
310
+ ### Durable background-job recovery after foreground interruption
311
+
312
+ `long-job-recovery-019` exercised installed AgInTiFlow `0.20.217` with a normal
313
+ laboratory-export request whose terminal could disconnect. The DeepSeek-backed
314
+ agent inspected the checked-in resumable scripts, rejected duplicate execution,
315
+ and selected the generic `start_long_job` routine. The campaign interrupted only
316
+ the foreground AgInTi pane after durable admission; the independent job tmux
317
+ continued, completed eight delayed batches, ran deferred verification, and
318
+ cleaned up its own tmux session.
319
+
320
+ The same AgInTi session then resumed from retained state. It recovered the exact
321
+ job ID through `long_job_status`, independently reran the project verifier,
322
+ confirmed the artifact and checksum, and finished without calling
323
+ `start_long_job` again. Proactive context compaction reduced the resumed history
324
+ from 190,805 to 30,680 characters while preserving the job identity and original
325
+ task boundary.
326
+
327
+ The independent acceptance contract verified one process invocation, one durable
328
+ job directory, attempt `1`, command and verifier exit `0`, the required durable
329
+ event sequence, a project handoff with exact status and verification commands,
330
+ and no lingering job tmux session. No source patch or npm release was needed;
331
+ this run validates the published package's general long-job, resume, context,
332
+ deduplication, and cleanup contracts without task-specific routing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.217",
3
+ "version": "0.20.218",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -583,6 +583,58 @@ try {
583
583
  ...hostWorkspacePolicy,
584
584
  readOnlyRoots: [externalReadRoot],
585
585
  };
586
+ const exactReadOnlySkillLoop = [
587
+ "for f in",
588
+ "/home/lachlan/.agintiflow/skillmesh/skills/lazyedit-publish-workflow/SKILL.md",
589
+ "/home/lachlan/.codex/skills/musia-music-production/SKILL.md",
590
+ "/home/lachlan/.codex/skills/musia-lalachan-mv-workflow/SKILL.md",
591
+ "/home/lachlan/.codex/skills/lalachan-xyq-browser-video/SKILL.md",
592
+ "/home/lachlan/.nvm/versions/node/v22.21.0/lib/node_modules/@lazyingart/agintiflow/skills/browser-automation/SKILL.md",
593
+ "/home/lachlan/.codex/skills/musia-song-localization/SKILL.md;",
594
+ 'do echo "===== $f ====="; wc -l "$f"; done',
595
+ ].join(" ");
596
+ const exactReadOnlySkillLoopPolicy = evaluateCommandPolicy(exactReadOnlySkillLoop, hostWorkspacePolicy);
597
+ assert(
598
+ exactReadOnlySkillLoopPolicy.allowed &&
599
+ exactReadOnlySkillLoopPolicy.category === "read-only" &&
600
+ exactReadOnlySkillLoopPolicy.boundedForLoop === true &&
601
+ exactReadOnlySkillLoopPolicy.needsNetwork === false &&
602
+ exactReadOnlySkillLoopPolicy.writesWorkspace === false,
603
+ "a finite echo/wc skill audit loop incorrectly required destructive host permission"
604
+ );
605
+ const multilineReadOnlyLoopPolicy = evaluateCommandPolicy(
606
+ [
607
+ "for target in README.md package.json",
608
+ "do",
609
+ ' echo "== ${target} =="',
610
+ ' wc -l "$target"',
611
+ "done",
612
+ ].join("\n"),
613
+ hostWorkspacePolicy
614
+ );
615
+ assert(
616
+ multilineReadOnlyLoopPolicy.allowed && multilineReadOnlyLoopPolicy.category === "read-only",
617
+ "a multiline finite read-only loop was not recognized"
618
+ );
619
+ for (const unsafeLoop of [
620
+ 'for f in README.md; do rm -rf "$f"; done',
621
+ 'for f in README.md; do curl "https://example.com/$f"; done',
622
+ 'for f in README.md; do cp "$f" copied.md; done',
623
+ 'for f in README.md; do echo "$f" > report.md; done',
624
+ 'for f in $(find . -type f); do wc -l "$f"; done',
625
+ 'for f in *.md; do wc -l "$f"; done',
626
+ 'for f in --pre=unsafe-helper; do rg pattern "$f"; done',
627
+ 'for f in $FILES; do wc -l "$f"; done',
628
+ 'for f in README.md; do wc -l "${f:-package.json}"; done',
629
+ 'for f in README.md; do for g in package.json; do wc -l "$g"; done; done',
630
+ 'for f in README.md; do wc -l "$f" & done',
631
+ ]) {
632
+ const unsafeLoopPolicy = evaluateCommandPolicy(unsafeLoop, hostWorkspacePolicy);
633
+ assert(
634
+ !unsafeLoopPolicy.allowed && unsafeLoopPolicy.category !== "read-only",
635
+ `unsafe or dynamic shell loop bypassed the bounded read-only policy: ${unsafeLoop}`
636
+ );
637
+ }
586
638
  const readRootDoctorPolicy = evaluateCommandPolicy(
587
639
  `cd ${externalReadRoot} && node bin/musia.js doctor --json 2>&1 | head -80`,
588
640
  hostReadRootPolicy
@@ -1372,6 +1372,100 @@ function classifyShellSequence(normalized) {
1372
1372
  };
1373
1373
  }
1374
1374
 
1375
+ const READ_ONLY_FOR_LOOP_MAX_ITEMS = 64;
1376
+ const READ_ONLY_FOR_LOOP_MAX_COMMANDS = 16;
1377
+ const READ_ONLY_FOR_LOOP_LITERAL_PATTERN = /^[A-Za-z0-9_@%+=:,./~+-]+$/;
1378
+
1379
+ function shellIdentifierPattern(value = "") {
1380
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1381
+ }
1382
+
1383
+ function broadForLoopClassification(normalized, reason) {
1384
+ return {
1385
+ category: "general-shell",
1386
+ needsNetwork: false,
1387
+ writesWorkspace: true,
1388
+ reason: `Shell for-loop is outside the bounded read-only form (${reason}): ${normalized}`,
1389
+ };
1390
+ }
1391
+
1392
+ function classifyReadOnlyForLoop(normalized) {
1393
+ const text = String(normalized || "").trim();
1394
+ if (!/^for\s+/.test(text)) return null;
1395
+ if (text.length > 64 * 1024) {
1396
+ return broadForLoopClassification(text, "command is too large");
1397
+ }
1398
+
1399
+ const match = text.match(
1400
+ /^for[ \t]+([A-Za-z_][A-Za-z0-9_]*)[ \t]+in[ \t]+([\s\S]*?)(?:;|\n)[ \t]*do\s+([\s\S]*?)(?:;|\n)[ \t]*done[ \t]*$/
1401
+ );
1402
+ if (!match) return broadForLoopClassification(text, "unsupported loop syntax");
1403
+
1404
+ const [, variableName, itemSource, bodySource] = match;
1405
+ if (hasActiveShellExpansion(itemSource) || hasActiveShellCommandSubstitution(itemSource)) {
1406
+ return broadForLoopClassification(text, "the item list is dynamic");
1407
+ }
1408
+ const items = tokenizeShellWords(itemSource);
1409
+ if (!items.length || items.length > READ_ONLY_FOR_LOOP_MAX_ITEMS) {
1410
+ return broadForLoopClassification(text, "the item list is empty or unbounded");
1411
+ }
1412
+ if (items.some((item) => item.startsWith("-") || !READ_ONLY_FOR_LOOP_LITERAL_PATTERN.test(item))) {
1413
+ return broadForLoopClassification(
1414
+ text,
1415
+ "items must be finite literal words or paths without option prefixes or globs"
1416
+ );
1417
+ }
1418
+
1419
+ const unquotedBody = stripQuotedSegments(bodySource);
1420
+ if (/[|&<>(){}]/.test(unquotedBody) || hasActiveShellCommandSubstitution(bodySource)) {
1421
+ return broadForLoopClassification(text, "the body uses control, redirection, or substitution syntax");
1422
+ }
1423
+
1424
+ const escapedVariable = shellIdentifierPattern(variableName);
1425
+ const variableReference = new RegExp(
1426
+ `\\$(?:\\{${escapedVariable}\\}|${escapedVariable}(?![A-Za-z0-9_]))`,
1427
+ "g"
1428
+ );
1429
+ const referenceCount = [...bodySource.matchAll(variableReference)].length;
1430
+ if (!referenceCount) {
1431
+ return broadForLoopClassification(text, "the body does not consume the loop item");
1432
+ }
1433
+
1434
+ for (const item of items) {
1435
+ const expandedBody = bodySource.replace(variableReference, item);
1436
+ if (hasActiveShellExpansion(expandedBody) || hasActiveShellCommandSubstitution(expandedBody)) {
1437
+ return broadForLoopClassification(text, "the body contains expansion beyond the loop variable");
1438
+ }
1439
+ const sequence = splitTopLevelShellSequence(expandedBody);
1440
+ const commands = sequence?.parts || [expandedBody.trim()];
1441
+ if (
1442
+ !commands.length ||
1443
+ commands.length > READ_ONLY_FOR_LOOP_MAX_COMMANDS ||
1444
+ (sequence && sequence.separators.some((separator) => ![";", "newline"].includes(separator)))
1445
+ ) {
1446
+ return broadForLoopClassification(text, "the body is not a bounded sequential command list");
1447
+ }
1448
+ for (const command of commands) {
1449
+ const classification = classifySimpleCommand(command);
1450
+ if (classification.category === "blocked" || classification.category === "destructive") {
1451
+ return { ...classification, gitOnly: false };
1452
+ }
1453
+ if (classification.category !== "read-only" || classification.writesWorkspace) {
1454
+ return broadForLoopClassification(text, `body command is ${classification.category}`);
1455
+ }
1456
+ }
1457
+ }
1458
+
1459
+ return {
1460
+ category: "read-only",
1461
+ needsNetwork: false,
1462
+ writesWorkspace: false,
1463
+ gitOnly: false,
1464
+ boundedForLoop: true,
1465
+ reason: `Finite read-only shell loop over ${items.length} literal item${items.length === 1 ? "" : "s"}.`,
1466
+ };
1467
+ }
1468
+
1375
1469
  function splitTopLevelPipeline(command = "") {
1376
1470
  const parts = [];
1377
1471
  let current = "";
@@ -1536,7 +1630,7 @@ export function classifyCommand(command) {
1536
1630
  const normalized = String(command || "").trim();
1537
1631
  if (!normalized) return { category: "blocked", reason: "Command is empty." };
1538
1632
 
1539
- return classifyBackgroundShell(normalized) || classifyCdCommand(normalized) || classifyShellSequence(normalized) || classifyPipelineSequence(normalized) || classifySimpleCommand(normalized);
1633
+ return classifyBackgroundShell(normalized) || classifyReadOnlyForLoop(normalized) || classifyCdCommand(normalized) || classifyShellSequence(normalized) || classifyPipelineSequence(normalized) || classifySimpleCommand(normalized);
1540
1634
  }
1541
1635
 
1542
1636
  export function evaluateCommandPolicy(command, config = {}) {